@chronicle.app/zotero 0.4.0 → 0.5.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.
@@ -1,4 +1,4 @@
1
- import { Record } from '@chronicle.app/etl';
1
+ import { Record, type Survey, type SurveyOptions } from '@chronicle.app/etl';
2
2
  import { SqliteExtractor } from '@chronicle.app/etl-sqlite';
3
3
  import ZoteroTransformer from './ZoteroTransformer.js';
4
4
  /**
@@ -69,5 +69,7 @@ export declare class ZoteroExtractor extends SqliteExtractor<typeof ZoteroExtrac
69
69
  private buildCollectionChain;
70
70
  private getItemCollections;
71
71
  determineCount(): Promise<number | null>;
72
+ /** The items a run reads of each record type, and when they were added. */
73
+ survey(options?: SurveyOptions): Promise<Survey | null>;
72
74
  }
73
75
  //# sourceMappingURL=ZoteroExtractor.d.ts.map
@@ -1,16 +1,56 @@
1
- import { SystemInfo, assertReadable } from '@chronicle.app/etl';
2
- import { SqliteExtractor, timeRangeConditions, iterateRows } from '@chronicle.app/etl-sqlite';
1
+ import { SystemInfo, assertReadable, } from '@chronicle.app/etl';
2
+ import { SqliteExtractor, allRows, countsByMonth, monthSql, timeRangeConditions, iterateRows, } from '@chronicle.app/etl-sqlite';
3
3
  import { DatabaseSync } from 'node:sqlite';
4
4
  import { createHash, randomUUID } from 'node:crypto';
5
5
  import { constants as fsConstants, copyFileSync, readFileSync, rmSync, statSync } from 'node:fs';
6
- import { homedir, tmpdir } from 'node:os';
6
+ import { tmpdir } from 'node:os';
7
7
  import { dirname, isAbsolute, join } from 'node:path';
8
8
  import { z } from 'zod';
9
- import ZoteroTransformer from './ZoteroTransformer.js';
9
+ import ZoteroTransformer, { parseZoteroDate } from './ZoteroTransformer.js';
10
+ import { zoteroDataDir } from '../dataDir.js';
10
11
  /** Zotero's UTC `YYYY-MM-DD HH:MM:SS` form, so string comparison against `dateModified` is correct. */
11
12
  function toSqliteTimestamp(d) {
12
13
  return d.toISOString().slice(0, 19).replace('T', ' ');
13
14
  }
15
+ /**
16
+ * The items behind each record type, as `FROM … WHERE …` before since/until:
17
+ * the user library's, and none in the trash or under an item in it.
18
+ */
19
+ const RECORD_ROWS = {
20
+ works: `
21
+ FROM items i
22
+ JOIN itemTypes it ON it.itemTypeID = i.itemTypeID
23
+ JOIN libraries l ON l.libraryID = i.libraryID
24
+ WHERE l.type = 'user'
25
+ AND i.itemID NOT IN (SELECT itemID FROM deletedItems)
26
+ AND it.typeName NOT IN ('attachment', 'annotation', 'note')`,
27
+ annotations: `
28
+ FROM itemAnnotations a
29
+ JOIN items i ON i.itemID = a.itemID
30
+ JOIN libraries l ON l.libraryID = i.libraryID
31
+ JOIN itemAttachments att ON att.itemID = a.parentItemID
32
+ JOIN items ai ON ai.itemID = att.itemID
33
+ WHERE l.type = 'user'
34
+ AND i.itemID NOT IN (SELECT itemID FROM deletedItems)
35
+ AND ai.itemID NOT IN (SELECT itemID FROM deletedItems)
36
+ AND (att.parentItemID IS NULL OR att.parentItemID NOT IN (SELECT itemID FROM deletedItems))`,
37
+ attachments: `
38
+ FROM itemAttachments att
39
+ JOIN items i ON i.itemID = att.itemID
40
+ JOIN libraries l ON l.libraryID = i.libraryID
41
+ WHERE l.type = 'user'
42
+ AND att.parentItemID IS NULL
43
+ AND i.itemID NOT IN (SELECT itemID FROM deletedItems)`,
44
+ notes: `
45
+ FROM items i
46
+ JOIN itemTypes it ON it.itemTypeID = i.itemTypeID
47
+ JOIN itemNotes n ON n.itemID = i.itemID
48
+ JOIN libraries l ON l.libraryID = i.libraryID
49
+ WHERE l.type = 'user'
50
+ AND it.typeName = 'note'
51
+ AND i.itemID NOT IN (SELECT itemID FROM deletedItems)
52
+ AND (n.parentItemID IS NULL OR n.parentItemID NOT IN (SELECT itemID FROM deletedItems))`,
53
+ };
14
54
  function isSqliteBusy(err) {
15
55
  return (typeof err === 'object' &&
16
56
  err !== null &&
@@ -42,7 +82,7 @@ export class ZoteroExtractor extends SqliteExtractor {
42
82
  input: z
43
83
  .string()
44
84
  .describe('Path to the Zotero data directory (contains zotero.sqlite and storage/)')
45
- .default(join(homedir(), 'Zotero')),
85
+ .default(() => zoteroDataDir()),
46
86
  });
47
87
  dataDir;
48
88
  localUserKey;
@@ -156,12 +196,7 @@ export class ZoteroExtractor extends SqliteExtractor {
156
196
  const sql = `
157
197
  SELECT i.itemID AS itemID, i.key AS key, i.dateAdded AS dateAdded, i.dateModified AS dateModified,
158
198
  it.typeName AS itemType
159
- FROM items i
160
- JOIN itemTypes it ON it.itemTypeID = i.itemTypeID
161
- JOIN libraries l ON l.libraryID = i.libraryID
162
- WHERE l.type = 'user'
163
- AND i.itemID NOT IN (SELECT itemID FROM deletedItems)
164
- AND it.typeName NOT IN ('attachment', 'annotation', 'note')${clause}
199
+ ${RECORD_ROWS.works}${clause}
165
200
  ORDER BY i.dateModified DESC`;
166
201
  for (const row of iterateRows(this.db.prepare(sql), ...params)) {
167
202
  if (this.shouldStopExtracting(this.extractedCount))
@@ -196,15 +231,7 @@ export class ZoteroExtractor extends SqliteExtractor {
196
231
  ai.itemID AS attItemID, ai.key AS attKey, ai.dateAdded AS attDateAdded,
197
232
  att.parentItemID AS workItemID, att.linkMode AS attLinkMode, att.contentType AS attContentType,
198
233
  att.path AS attPath, att.lastRead AS attLastRead
199
- FROM itemAnnotations a
200
- JOIN items i ON i.itemID = a.itemID
201
- JOIN libraries l ON l.libraryID = i.libraryID
202
- JOIN itemAttachments att ON att.itemID = a.parentItemID
203
- JOIN items ai ON ai.itemID = att.itemID
204
- WHERE l.type = 'user'
205
- AND i.itemID NOT IN (SELECT itemID FROM deletedItems)
206
- AND ai.itemID NOT IN (SELECT itemID FROM deletedItems)
207
- AND (att.parentItemID IS NULL OR att.parentItemID NOT IN (SELECT itemID FROM deletedItems))${clause}
234
+ ${RECORD_ROWS.annotations}${clause}
208
235
  ORDER BY i.dateModified DESC`;
209
236
  for (const row of iterateRows(this.db.prepare(sql), ...params)) {
210
237
  if (this.shouldStopExtracting(this.extractedCount))
@@ -251,12 +278,7 @@ export class ZoteroExtractor extends SqliteExtractor {
251
278
  SELECT i.itemID AS itemID, i.key AS key, i.dateAdded AS dateAdded, i.dateModified AS dateModified,
252
279
  att.parentItemID AS parentItemID, att.linkMode AS linkMode, att.contentType AS contentType,
253
280
  att.path AS path, att.lastRead AS lastRead
254
- FROM itemAttachments att
255
- JOIN items i ON i.itemID = att.itemID
256
- JOIN libraries l ON l.libraryID = i.libraryID
257
- WHERE l.type = 'user'
258
- AND att.parentItemID IS NULL
259
- AND i.itemID NOT IN (SELECT itemID FROM deletedItems)${clause}
281
+ ${RECORD_ROWS.attachments}${clause}
260
282
  ORDER BY i.dateModified DESC`;
261
283
  for (const row of iterateRows(this.db.prepare(sql), ...params)) {
262
284
  if (this.shouldStopExtracting(this.extractedCount))
@@ -281,14 +303,7 @@ export class ZoteroExtractor extends SqliteExtractor {
281
303
  const sql = `
282
304
  SELECT i.itemID AS itemID, i.key AS key, i.dateAdded AS dateAdded, i.dateModified AS dateModified,
283
305
  n.parentItemID AS parentItemID, n.note AS note, n.title AS title
284
- FROM items i
285
- JOIN itemTypes it ON it.itemTypeID = i.itemTypeID
286
- JOIN itemNotes n ON n.itemID = i.itemID
287
- JOIN libraries l ON l.libraryID = i.libraryID
288
- WHERE l.type = 'user'
289
- AND it.typeName = 'note'
290
- AND i.itemID NOT IN (SELECT itemID FROM deletedItems)
291
- AND (n.parentItemID IS NULL OR n.parentItemID NOT IN (SELECT itemID FROM deletedItems))${clause}
306
+ ${RECORD_ROWS.notes}${clause}
292
307
  ORDER BY i.dateModified DESC`;
293
308
  for (const row of iterateRows(this.db.prepare(sql), ...params)) {
294
309
  if (this.shouldStopExtracting(this.extractedCount))
@@ -498,55 +513,39 @@ export class ZoteroExtractor extends SqliteExtractor {
498
513
  if (!this.db)
499
514
  return null;
500
515
  try {
501
- const worksFilter = this.sinceUntilFilter('i.dateModified');
502
- const works = this.db
503
- .prepare(`SELECT COUNT(*) AS n
504
- FROM items i
505
- JOIN itemTypes it ON it.itemTypeID = i.itemTypeID
506
- JOIN libraries l ON l.libraryID = i.libraryID
507
- WHERE l.type = 'user'
508
- AND i.itemID NOT IN (SELECT itemID FROM deletedItems)
509
- AND it.typeName NOT IN ('attachment', 'annotation', 'note')${worksFilter.clause}`)
510
- .get(...worksFilter.params);
511
- const annotationsFilter = this.sinceUntilFilter('i.dateModified');
512
- const annotations = this.db
513
- .prepare(`SELECT COUNT(*) AS n
514
- FROM itemAnnotations a
515
- JOIN items i ON i.itemID = a.itemID
516
- JOIN libraries l ON l.libraryID = i.libraryID
517
- JOIN itemAttachments att ON att.itemID = a.parentItemID
518
- JOIN items ai ON ai.itemID = att.itemID
519
- WHERE l.type = 'user'
520
- AND i.itemID NOT IN (SELECT itemID FROM deletedItems)
521
- AND ai.itemID NOT IN (SELECT itemID FROM deletedItems)
522
- AND (att.parentItemID IS NULL OR att.parentItemID NOT IN (SELECT itemID FROM deletedItems))${annotationsFilter.clause}`)
523
- .get(...annotationsFilter.params);
524
- const attachmentsFilter = this.sinceUntilFilter('i.dateModified');
525
- const attachments = this.db
526
- .prepare(`SELECT COUNT(*) AS n
527
- FROM itemAttachments att
528
- JOIN items i ON i.itemID = att.itemID
529
- JOIN libraries l ON l.libraryID = i.libraryID
530
- WHERE l.type = 'user'
531
- AND att.parentItemID IS NULL
532
- AND i.itemID NOT IN (SELECT itemID FROM deletedItems)${attachmentsFilter.clause}`)
533
- .get(...attachmentsFilter.params);
534
- const notesFilter = this.sinceUntilFilter('i.dateModified');
535
- const notes = this.db
536
- .prepare(`SELECT COUNT(*) AS n
537
- FROM items i
538
- JOIN itemTypes it ON it.itemTypeID = i.itemTypeID
539
- JOIN itemNotes n ON n.itemID = i.itemID
540
- JOIN libraries l ON l.libraryID = i.libraryID
541
- WHERE l.type = 'user'
542
- AND it.typeName = 'note'
543
- AND i.itemID NOT IN (SELECT itemID FROM deletedItems)
544
- AND (n.parentItemID IS NULL OR n.parentItemID NOT IN (SELECT itemID FROM deletedItems))${notesFilter.clause}`)
545
- .get(...notesFilter.params);
546
- return works.n + annotations.n + attachments.n + notes.n;
516
+ const { clause, params } = this.sinceUntilFilter('i.dateModified');
517
+ let total = 0;
518
+ for (const rows of Object.values(RECORD_ROWS)) {
519
+ const { n } = this.db.prepare(`SELECT COUNT(*) AS n ${rows}${clause}`).get(...params);
520
+ total += n;
521
+ }
522
+ return total;
547
523
  }
548
524
  catch {
549
525
  return null;
550
526
  }
551
527
  }
528
+ /** The items a run reads of each record type, and when they were added. */
529
+ async survey(options = {}) {
530
+ if (!this.db)
531
+ return null;
532
+ const { clause, params } = this.sinceUntilFilter('i.dateModified');
533
+ // dateAdded is UTC text, so its month is from its Unix seconds.
534
+ const month = monthSql("CAST(strftime('%s', i.dateAdded) AS INTEGER)");
535
+ const survey = {};
536
+ for (const [recordType, rows] of Object.entries(RECORD_ROWS)) {
537
+ const { n, first, last } = this.db
538
+ .prepare(`SELECT COUNT(*) AS n, MIN(i.dateAdded) AS first, MAX(i.dateAdded) AS last ${rows}${clause}`)
539
+ .get(...params);
540
+ const byMonth = options.byMonth &&
541
+ countsByMonth(allRows(this.db.prepare(`SELECT ${month} AS month, COUNT(*) AS n ${rows}${clause} GROUP BY month`), ...params));
542
+ survey[recordType] = {
543
+ count: n,
544
+ ...(first !== null && { earliest: parseZoteroDate(first) }),
545
+ ...(last !== null && { latest: parseZoteroDate(last) }),
546
+ ...(byMonth && { byMonth }),
547
+ };
548
+ }
549
+ return survey;
550
+ }
552
551
  }
@@ -0,0 +1,12 @@
1
+ import { Locator, type Candidate, type LocationMatch } from '@chronicle.app/etl';
2
+ /**
3
+ * Zotero's library where its preferences say it is, which is where a run
4
+ * reads it, and one left in the default `~/Zotero` the manifest names, if
5
+ * that's another.
6
+ */
7
+ export declare class ZoteroLocator extends Locator {
8
+ static source: string;
9
+ static strategy: string;
10
+ locate(matches: LocationMatch[]): AsyncGenerator<Candidate>;
11
+ }
12
+ //# sourceMappingURL=ZoteroLocator.d.ts.map
@@ -0,0 +1,20 @@
1
+ import { dirname, join } from 'node:path';
2
+ import { Locator } from '@chronicle.app/etl';
3
+ import { zoteroDataDir } from '../dataDir.js';
4
+ /**
5
+ * Zotero's library where its preferences say it is, which is where a run
6
+ * reads it, and one left in the default `~/Zotero` the manifest names, if
7
+ * that's another.
8
+ */
9
+ export class ZoteroLocator extends Locator {
10
+ static source = 'zotero';
11
+ static strategy = 'app-db';
12
+ async *locate(matches) {
13
+ const libraries = new Set([
14
+ join(zoteroDataDir(this.context.home), 'zotero.sqlite'),
15
+ ...matches.map(match => match.path),
16
+ ]);
17
+ for (const file of libraries)
18
+ yield { config: { input: dirname(file) }, path: file };
19
+ }
20
+ }
@@ -1,5 +1,7 @@
1
1
  import { ChronicleTransformer, Record } from '@chronicle.app/etl';
2
2
  import { ActionAndChildren } from '@chronicle.app/schema';
3
+ /** Zotero timestamps are UTC `YYYY-MM-DD HH:MM:SS`, no zone marker. */
4
+ export declare function parseZoteroDate(raw: string): Date;
3
5
  /**
4
6
  * A local Zotero library (`zotero.sqlite`) → Chronicle JSON-LD.
5
7
  *
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import { ChronicleTransformer } from '@chronicle.app/etl';
3
3
  const SOURCE = 'zotero';
4
4
  /** Zotero timestamps are UTC `YYYY-MM-DD HH:MM:SS`, no zone marker. */
5
- function parseZoteroDate(raw) {
5
+ export function parseZoteroDate(raw) {
6
6
  return new Date(`${raw.replace(' ', 'T')}Z`);
7
7
  }
8
8
  /** Zotero itemType → the Chronicle work subtype (every value here exists in the schema). */
@@ -239,9 +239,9 @@ export default class ZoteroTransformer extends ChronicleTransformer {
239
239
  const actions = [
240
240
  {
241
241
  '@type': 'BookmarkAction',
242
- '@key': ['@type', 'source', 'sourceId', 'object.inRealm.handle'],
242
+ // Keyed by the work it saved.
243
+ '@key': ['@type', 'source', 'object.sourceId', 'object.inRealm.handle'],
243
244
  source: SOURCE,
244
- sourceId: work.key,
245
245
  timestamp: parseZoteroDate(work.dateAdded),
246
246
  agent: me,
247
247
  object: workEntity,
@@ -255,7 +255,7 @@ export default class ZoteroTransformer extends ChronicleTransformer {
255
255
  // so each viewing independently witnesses the merge regardless of
256
256
  // fold order.
257
257
  const facet = this.buildFileFacet(attachment, context, workRef);
258
- actions.push(this.buildViewAction(attachment.key, attachment.lastRead, facet, me));
258
+ actions.push(this.buildViewAction(attachment.lastRead, facet, me));
259
259
  }
260
260
  }
261
261
  return actions;
@@ -270,12 +270,12 @@ export default class ZoteroTransformer extends ChronicleTransformer {
270
270
  * "currently reading" state. A single sighted open is one-shot
271
271
  * consumption; `ViewAction` is its honest weight.
272
272
  */
273
- buildViewAction(sourceId, lastRead, object, agent) {
273
+ buildViewAction(lastRead, object, agent) {
274
274
  return {
275
275
  '@type': 'ViewAction',
276
- '@key': ['@type', 'source', 'sourceId', 'object.inRealm.handle', 'timestamp'],
276
+ // Keyed by the file it opened, and when.
277
+ '@key': ['@type', 'source', 'object.sourceId', 'object.inRealm.handle', 'timestamp'],
277
278
  source: SOURCE,
278
- sourceId,
279
279
  timestamp: new Date(lastRead * 1000),
280
280
  agent,
281
281
  object,
@@ -754,29 +754,37 @@ export default class ZoteroTransformer extends ChronicleTransformer {
754
754
  quotation.selector = [selector];
755
755
  actions.push({
756
756
  '@type': 'QuoteAction',
757
- '@key': ['@type', 'source', 'sourceId', 'result.inRealm.handle'],
757
+ // Keyed by the quotation it made, which has the annotation's key.
758
+ '@key': ['@type', 'source', 'result.sourceId', 'result.inRealm.handle'],
758
759
  source: SOURCE,
759
- sourceId: annotation.key,
760
760
  timestamp: parseZoteroDate(annotation.dateAdded),
761
761
  agent: me,
762
762
  result: quotation,
763
763
  });
764
764
  }
765
765
  if (hasComment) {
766
+ // A comment on a highlight is keyed by the quotation it's about, which
767
+ // has the annotation's key. A comment on its own has the key itself.
766
768
  const comment = {
767
769
  '@type': 'Comment',
768
- '@key': ['@type', 'source', 'sourceId', 'inRealm.handle'],
770
+ ...(quotation
771
+ ? { '@key': ['@type', 'source', 'about[*].sourceId', 'inRealm.handle'] }
772
+ : {
773
+ '@key': ['@type', 'source', 'sourceId', 'inRealm.handle'],
774
+ sourceId: annotation.key,
775
+ }),
769
776
  source: SOURCE,
770
- sourceId: annotation.key,
771
777
  inRealm: realm,
772
778
  description: annotation.comment,
773
779
  about: [quotation ?? subjectRef],
774
780
  };
775
781
  actions.push({
776
782
  '@type': 'AnnotateAction',
777
- '@key': ['@type', 'source', 'sourceId', 'result.inRealm.handle'],
783
+ // Keyed by the comment it made.
784
+ '@key': quotation
785
+ ? ['@type', 'source', 'result.about[*].sourceId', 'result.inRealm.handle']
786
+ : ['@type', 'source', 'result.sourceId', 'result.inRealm.handle'],
778
787
  source: SOURCE,
779
- sourceId: annotation.key,
780
788
  timestamp: parseZoteroDate(annotation.dateAdded),
781
789
  agent: me,
782
790
  result: comment,
@@ -831,16 +839,16 @@ export default class ZoteroTransformer extends ChronicleTransformer {
831
839
  const actions = [
832
840
  {
833
841
  '@type': 'BookmarkAction',
834
- '@key': ['@type', 'source', 'sourceId', 'object.inRealm.handle'],
842
+ // Keyed by the file it saved.
843
+ '@key': ['@type', 'source', 'object.sourceId', 'object.inRealm.handle'],
835
844
  source: SOURCE,
836
- sourceId: attachment.key,
837
845
  timestamp: parseZoteroDate(attachment.dateAdded),
838
846
  agent: me,
839
847
  object: entity,
840
848
  },
841
849
  ];
842
850
  if (attachment.lastRead) {
843
- actions.push(this.buildViewAction(attachment.key, attachment.lastRead, this.buildAttachmentRef(attachment, context), me));
851
+ actions.push(this.buildViewAction(attachment.lastRead, this.buildAttachmentRef(attachment, context), me));
844
852
  }
845
853
  return actions;
846
854
  }
@@ -929,9 +937,9 @@ export default class ZoteroTransformer extends ChronicleTransformer {
929
937
  return [
930
938
  {
931
939
  '@type': 'AnnotateAction',
932
- '@key': ['@type', 'source', 'sourceId', 'result.inRealm.handle'],
940
+ // Keyed by the note it made.
941
+ '@key': ['@type', 'source', 'result.sourceId', 'result.inRealm.handle'],
933
942
  source: SOURCE,
934
- sourceId: note.key,
935
943
  timestamp: parseZoteroDate(note.dateAdded),
936
944
  agent: me,
937
945
  result: comment,
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Zotero's data folder: the one a profile's preferences name, when it was
3
+ * moved from the default, else `~/Zotero`. Read the way Zotero reads it, so
4
+ * a run and a scan find the same library.
5
+ */
6
+ export declare function zoteroDataDir(home?: string): string;
7
+ //# sourceMappingURL=dataDir.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ /** Where Zotero keeps its profiles, on macOS and on Linux. */
5
+ const PROFILES = [
6
+ ['Library', 'Application Support', 'Zotero', 'Profiles'],
7
+ ['.zotero', 'zotero'],
8
+ ];
9
+ /** A preference's value from a profile's `prefs.js`, as the JSON Zotero writes it. */
10
+ function pref(prefs, name) {
11
+ const value = prefs.match(new RegExp(`user_pref\\("${name.replaceAll('.', '\\.')}",\\s*(.+?)\\);`));
12
+ if (!value)
13
+ return undefined;
14
+ try {
15
+ return JSON.parse(value[1]);
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ /**
22
+ * Zotero's data folder: the one a profile's preferences name, when it was
23
+ * moved from the default, else `~/Zotero`. Read the way Zotero reads it, so
24
+ * a run and a scan find the same library.
25
+ */
26
+ export function zoteroDataDir(home = homedir()) {
27
+ for (const parts of PROFILES) {
28
+ const root = join(home, ...parts);
29
+ let profiles;
30
+ try {
31
+ profiles = readdirSync(root).sort();
32
+ }
33
+ catch {
34
+ continue;
35
+ }
36
+ for (const profile of profiles) {
37
+ let prefs;
38
+ try {
39
+ prefs = readFileSync(join(root, profile, 'prefs.js'), 'utf8');
40
+ }
41
+ catch {
42
+ continue;
43
+ }
44
+ const dir = pref(prefs, 'extensions.zotero.dataDir');
45
+ if (pref(prefs, 'extensions.zotero.useDataDir') === true && typeof dir === 'string') {
46
+ return dir;
47
+ }
48
+ }
49
+ }
50
+ return join(home, 'Zotero');
51
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { ZoteroExtractor } from './connectors/ZoteroExtractor.js';
2
+ export { ZoteroLocator } from './connectors/ZoteroLocator.js';
3
+ export { zoteroDataDir } from './dataDir.js';
2
4
  export { default as ZoteroTransformer } from './connectors/ZoteroTransformer.js';
3
5
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,2 +1,4 @@
1
1
  export { ZoteroExtractor } from './connectors/ZoteroExtractor.js';
2
+ export { ZoteroLocator } from './connectors/ZoteroLocator.js';
3
+ export { zoteroDataDir } from './dataDir.js';
2
4
  export { default as ZoteroTransformer } from './connectors/ZoteroTransformer.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chronicle.app/zotero",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -22,9 +22,9 @@
22
22
  "type": "module",
23
23
  "description": "Chronicle plugin for a local Zotero library: works, highlights, notes, and reading events",
24
24
  "devDependencies": {
25
- "@chronicle.app/etl": "0.4.0",
26
- "@chronicle.app/schema": "0.4.0",
27
- "@chronicle.app/tsconfig": "0.4.0",
25
+ "@chronicle.app/etl": "0.5.0",
26
+ "@chronicle.app/schema": "0.5.0",
27
+ "@chronicle.app/tsconfig": "0.5.0",
28
28
  "@types/node": "^22.10.1",
29
29
  "typescript": "^5.7.2"
30
30
  },
@@ -39,7 +39,7 @@
39
39
  "registry": "https://registry.npmjs.org/"
40
40
  },
41
41
  "dependencies": {
42
- "@chronicle.app/etl-sqlite": "0.4.0",
42
+ "@chronicle.app/etl-sqlite": "0.5.0",
43
43
  "zod": "^3.24.1"
44
44
  },
45
45
  "peerDependencies": {
@@ -68,7 +68,12 @@
68
68
  "attachments",
69
69
  "notes"
70
70
  ],
71
- "default": true
71
+ "default": true,
72
+ "locations": [
73
+ {
74
+ "path": "~/Zotero/zotero.sqlite"
75
+ }
76
+ ]
72
77
  }
73
78
  }
74
79
  }