@ncukondo/reference-manager 0.1.0 → 0.4.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.
Files changed (168) hide show
  1. package/README.md +278 -80
  2. package/dist/chunks/{detector-BF8Mcc72.js → file-watcher-B-SiUw5f.js} +469 -327
  3. package/dist/chunks/file-watcher-B-SiUw5f.js.map +1 -0
  4. package/dist/chunks/index-DLIGxQaB.js +29851 -0
  5. package/dist/chunks/index-DLIGxQaB.js.map +1 -0
  6. package/dist/chunks/loader-DuzyKV70.js +394 -0
  7. package/dist/chunks/loader-DuzyKV70.js.map +1 -0
  8. package/dist/cli/commands/add.d.ts +44 -16
  9. package/dist/cli/commands/add.d.ts.map +1 -1
  10. package/dist/cli/commands/cite.d.ts +49 -0
  11. package/dist/cli/commands/cite.d.ts.map +1 -0
  12. package/dist/cli/commands/fulltext.d.ts +72 -0
  13. package/dist/cli/commands/fulltext.d.ts.map +1 -0
  14. package/dist/cli/commands/index.d.ts +14 -10
  15. package/dist/cli/commands/index.d.ts.map +1 -1
  16. package/dist/cli/commands/list.d.ts +23 -6
  17. package/dist/cli/commands/list.d.ts.map +1 -1
  18. package/dist/cli/commands/mcp.d.ts +16 -0
  19. package/dist/cli/commands/mcp.d.ts.map +1 -0
  20. package/dist/cli/commands/remove.d.ts +47 -12
  21. package/dist/cli/commands/remove.d.ts.map +1 -1
  22. package/dist/cli/commands/search.d.ts +24 -7
  23. package/dist/cli/commands/search.d.ts.map +1 -1
  24. package/dist/cli/commands/server.d.ts +2 -0
  25. package/dist/cli/commands/server.d.ts.map +1 -1
  26. package/dist/cli/commands/update.d.ts +26 -13
  27. package/dist/cli/commands/update.d.ts.map +1 -1
  28. package/dist/cli/execution-context.d.ts +47 -0
  29. package/dist/cli/execution-context.d.ts.map +1 -0
  30. package/dist/cli/helpers.d.ts +18 -0
  31. package/dist/cli/helpers.d.ts.map +1 -1
  32. package/dist/cli/index.d.ts.map +1 -1
  33. package/dist/cli/server-client.d.ts +61 -14
  34. package/dist/cli/server-client.d.ts.map +1 -1
  35. package/dist/cli/server-detection.d.ts +1 -0
  36. package/dist/cli/server-detection.d.ts.map +1 -1
  37. package/dist/cli.js +21979 -564
  38. package/dist/cli.js.map +1 -1
  39. package/dist/config/csl-styles.d.ts +83 -0
  40. package/dist/config/csl-styles.d.ts.map +1 -0
  41. package/dist/config/defaults.d.ts +10 -0
  42. package/dist/config/defaults.d.ts.map +1 -1
  43. package/dist/config/loader.d.ts.map +1 -1
  44. package/dist/config/schema.d.ts +86 -3
  45. package/dist/config/schema.d.ts.map +1 -1
  46. package/dist/core/csl-json/types.d.ts +18 -3
  47. package/dist/core/csl-json/types.d.ts.map +1 -1
  48. package/dist/core/library-interface.d.ts +100 -0
  49. package/dist/core/library-interface.d.ts.map +1 -0
  50. package/dist/core/library.d.ts +56 -13
  51. package/dist/core/library.d.ts.map +1 -1
  52. package/dist/features/format/bibtex.d.ts +6 -0
  53. package/dist/features/format/bibtex.d.ts.map +1 -0
  54. package/dist/features/format/citation-csl.d.ts +41 -0
  55. package/dist/features/format/citation-csl.d.ts.map +1 -0
  56. package/dist/features/format/citation-fallback.d.ts +24 -0
  57. package/dist/features/format/citation-fallback.d.ts.map +1 -0
  58. package/dist/features/format/index.d.ts +10 -0
  59. package/dist/features/format/index.d.ts.map +1 -0
  60. package/dist/features/format/json.d.ts +6 -0
  61. package/dist/features/format/json.d.ts.map +1 -0
  62. package/dist/features/format/pretty.d.ts +6 -0
  63. package/dist/features/format/pretty.d.ts.map +1 -0
  64. package/dist/features/fulltext/filename.d.ts +17 -0
  65. package/dist/features/fulltext/filename.d.ts.map +1 -0
  66. package/dist/features/fulltext/index.d.ts +7 -0
  67. package/dist/features/fulltext/index.d.ts.map +1 -0
  68. package/dist/features/fulltext/manager.d.ts +109 -0
  69. package/dist/features/fulltext/manager.d.ts.map +1 -0
  70. package/dist/features/fulltext/types.d.ts +12 -0
  71. package/dist/features/fulltext/types.d.ts.map +1 -0
  72. package/dist/features/import/cache.d.ts +37 -0
  73. package/dist/features/import/cache.d.ts.map +1 -0
  74. package/dist/features/import/detector.d.ts +42 -0
  75. package/dist/features/import/detector.d.ts.map +1 -0
  76. package/dist/features/import/fetcher.d.ts +49 -0
  77. package/dist/features/import/fetcher.d.ts.map +1 -0
  78. package/dist/features/import/importer.d.ts +61 -0
  79. package/dist/features/import/importer.d.ts.map +1 -0
  80. package/dist/features/import/index.d.ts +8 -0
  81. package/dist/features/import/index.d.ts.map +1 -0
  82. package/dist/features/import/normalizer.d.ts +15 -0
  83. package/dist/features/import/normalizer.d.ts.map +1 -0
  84. package/dist/features/import/parser.d.ts +33 -0
  85. package/dist/features/import/parser.d.ts.map +1 -0
  86. package/dist/features/import/rate-limiter.d.ts +45 -0
  87. package/dist/features/import/rate-limiter.d.ts.map +1 -0
  88. package/dist/features/operations/add.d.ts +65 -0
  89. package/dist/features/operations/add.d.ts.map +1 -0
  90. package/dist/features/operations/cite.d.ts +48 -0
  91. package/dist/features/operations/cite.d.ts.map +1 -0
  92. package/dist/features/operations/fulltext/attach.d.ts +47 -0
  93. package/dist/features/operations/fulltext/attach.d.ts.map +1 -0
  94. package/dist/features/operations/fulltext/detach.d.ts +38 -0
  95. package/dist/features/operations/fulltext/detach.d.ts.map +1 -0
  96. package/dist/features/operations/fulltext/get.d.ts +41 -0
  97. package/dist/features/operations/fulltext/get.d.ts.map +1 -0
  98. package/dist/features/operations/fulltext/index.d.ts +9 -0
  99. package/dist/features/operations/fulltext/index.d.ts.map +1 -0
  100. package/dist/features/operations/index.d.ts +15 -0
  101. package/dist/features/operations/index.d.ts.map +1 -0
  102. package/dist/features/operations/library-operations.d.ts +64 -0
  103. package/dist/features/operations/library-operations.d.ts.map +1 -0
  104. package/dist/features/operations/list.d.ts +28 -0
  105. package/dist/features/operations/list.d.ts.map +1 -0
  106. package/dist/features/operations/operations-library.d.ts +36 -0
  107. package/dist/features/operations/operations-library.d.ts.map +1 -0
  108. package/dist/features/operations/remove.d.ts +29 -0
  109. package/dist/features/operations/remove.d.ts.map +1 -0
  110. package/dist/features/operations/search.d.ts +30 -0
  111. package/dist/features/operations/search.d.ts.map +1 -0
  112. package/dist/features/operations/update.d.ts +39 -0
  113. package/dist/features/operations/update.d.ts.map +1 -0
  114. package/dist/features/search/matcher.d.ts.map +1 -1
  115. package/dist/features/search/normalizer.d.ts +12 -0
  116. package/dist/features/search/normalizer.d.ts.map +1 -1
  117. package/dist/features/search/tokenizer.d.ts.map +1 -1
  118. package/dist/features/search/types.d.ts +1 -1
  119. package/dist/features/search/types.d.ts.map +1 -1
  120. package/dist/features/search/uppercase.d.ts +41 -0
  121. package/dist/features/search/uppercase.d.ts.map +1 -0
  122. package/dist/index.js +21 -187
  123. package/dist/index.js.map +1 -1
  124. package/dist/mcp/context.d.ts +19 -0
  125. package/dist/mcp/context.d.ts.map +1 -0
  126. package/dist/mcp/index.d.ts +20 -0
  127. package/dist/mcp/index.d.ts.map +1 -0
  128. package/dist/mcp/resources/index.d.ts +10 -0
  129. package/dist/mcp/resources/index.d.ts.map +1 -0
  130. package/dist/mcp/resources/library.d.ts +26 -0
  131. package/dist/mcp/resources/library.d.ts.map +1 -0
  132. package/dist/mcp/tools/add.d.ts +17 -0
  133. package/dist/mcp/tools/add.d.ts.map +1 -0
  134. package/dist/mcp/tools/cite.d.ts +15 -0
  135. package/dist/mcp/tools/cite.d.ts.map +1 -0
  136. package/dist/mcp/tools/fulltext.d.ts +51 -0
  137. package/dist/mcp/tools/fulltext.d.ts.map +1 -0
  138. package/dist/mcp/tools/index.d.ts +12 -0
  139. package/dist/mcp/tools/index.d.ts.map +1 -0
  140. package/dist/mcp/tools/list.d.ts +13 -0
  141. package/dist/mcp/tools/list.d.ts.map +1 -0
  142. package/dist/mcp/tools/remove.d.ts +19 -0
  143. package/dist/mcp/tools/remove.d.ts.map +1 -0
  144. package/dist/mcp/tools/search.d.ts +13 -0
  145. package/dist/mcp/tools/search.d.ts.map +1 -0
  146. package/dist/server/index.d.ts +26 -2
  147. package/dist/server/index.d.ts.map +1 -1
  148. package/dist/server/routes/add.d.ts +11 -0
  149. package/dist/server/routes/add.d.ts.map +1 -0
  150. package/dist/server/routes/cite.d.ts +9 -0
  151. package/dist/server/routes/cite.d.ts.map +1 -0
  152. package/dist/server/routes/list.d.ts +25 -0
  153. package/dist/server/routes/list.d.ts.map +1 -0
  154. package/dist/server/routes/references.d.ts.map +1 -1
  155. package/dist/server/routes/search.d.ts +26 -0
  156. package/dist/server/routes/search.d.ts.map +1 -0
  157. package/dist/server.js +5 -88
  158. package/dist/server.js.map +1 -1
  159. package/package.json +16 -4
  160. package/dist/chunks/detector-BF8Mcc72.js.map +0 -1
  161. package/dist/cli/output/bibtex.d.ts +0 -6
  162. package/dist/cli/output/bibtex.d.ts.map +0 -1
  163. package/dist/cli/output/index.d.ts +0 -7
  164. package/dist/cli/output/index.d.ts.map +0 -1
  165. package/dist/cli/output/json.d.ts +0 -6
  166. package/dist/cli/output/json.d.ts.map +0 -1
  167. package/dist/cli/output/pretty.d.ts +0 -6
  168. package/dist/cli/output/pretty.d.ts.map +0 -1
@@ -1,10 +1,12 @@
1
1
  import { randomUUID, createHash } from "node:crypto";
2
- import { createReadStream, existsSync, readFileSync } from "node:fs";
2
+ import { createReadStream } from "node:fs";
3
+ import * as fs from "node:fs/promises";
3
4
  import { readFile, mkdir, writeFile } from "node:fs/promises";
4
5
  import { z } from "zod";
5
- import { dirname, join } from "node:path";
6
- import { parse } from "@iarna/toml";
7
- import { tmpdir, homedir } from "node:os";
6
+ import * as path from "node:path";
7
+ import { dirname } from "node:path";
8
+ import { EventEmitter } from "node:events";
9
+ import chokidar from "chokidar";
8
10
  function normalizeText(text) {
9
11
  return text.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_|_$/g, "");
10
12
  }
@@ -310,12 +312,18 @@ const CslDateSchema = z.object({
310
312
  circa: z.boolean().optional(),
311
313
  literal: z.string().optional()
312
314
  });
315
+ const CslFulltextSchema = z.object({
316
+ pdf: z.string().optional(),
317
+ markdown: z.string().optional()
318
+ });
313
319
  const CslCustomSchema = z.object({
314
320
  uuid: z.string(),
315
321
  created_at: z.string(),
316
322
  timestamp: z.string(),
317
- additional_urls: z.array(z.string()).optional()
318
- });
323
+ additional_urls: z.array(z.string()).optional(),
324
+ fulltext: CslFulltextSchema.optional(),
325
+ tags: z.array(z.string()).optional()
326
+ }).passthrough();
319
327
  const CslItemSchema = z.object({
320
328
  id: z.string(),
321
329
  type: z.string(),
@@ -449,46 +457,83 @@ class Library {
449
457
  await writeCslJson(this.filePath, items);
450
458
  this.currentHash = await computeFileHash(this.filePath);
451
459
  }
460
+ /**
461
+ * Reloads the library from file if it was modified externally.
462
+ * Self-writes (detected via hash comparison) are skipped.
463
+ * @returns true if reload occurred, false if skipped (self-write detected)
464
+ */
465
+ async reload() {
466
+ const newHash = await computeFileHash(this.filePath);
467
+ if (newHash === this.currentHash) {
468
+ return false;
469
+ }
470
+ const items = await parseCslJson(this.filePath);
471
+ this.references = [];
472
+ this.uuidIndex.clear();
473
+ this.idIndex.clear();
474
+ this.doiIndex.clear();
475
+ this.pmidIndex.clear();
476
+ for (const item of items) {
477
+ const ref = new Reference(item);
478
+ this.references.push(ref);
479
+ this.addToIndices(ref);
480
+ }
481
+ this.currentHash = newHash;
482
+ return true;
483
+ }
452
484
  /**
453
485
  * Add a reference to the library
486
+ * @param item - The CSL item to add
487
+ * @returns The added CSL item (with generated ID and UUID)
454
488
  */
455
- add(item) {
489
+ async add(item) {
456
490
  const existingIds = new Set(this.references.map((ref2) => ref2.getId()));
457
491
  const ref = Reference.create(item, { existingIds });
458
492
  this.references.push(ref);
459
493
  this.addToIndices(ref);
494
+ return ref.getItem();
460
495
  }
461
496
  /**
462
- * Remove a reference by UUID
497
+ * Remove a reference by citation ID or UUID.
498
+ * @param identifier - The citation ID or UUID of the reference to remove
499
+ * @param options - Remove options (byUuid to use UUID lookup)
500
+ * @returns Remove result with removed status and the removed item
463
501
  */
464
- removeByUuid(uuid) {
465
- const ref = this.uuidIndex.get(uuid);
502
+ async remove(identifier, options = {}) {
503
+ const { byUuid = false } = options;
504
+ const ref = byUuid ? this.uuidIndex.get(identifier) : this.idIndex.get(identifier);
466
505
  if (!ref) {
467
- return false;
506
+ return { removed: false };
468
507
  }
469
- return this.removeReference(ref);
508
+ const removedItem = ref.getItem();
509
+ const removed = this.removeReference(ref);
510
+ return { removed, removedItem };
470
511
  }
471
512
  /**
472
- * Remove a reference by ID
513
+ * Update a reference by citation ID or UUID.
514
+ * @param identifier - The citation ID or UUID of the reference to update
515
+ * @param updates - Partial updates to apply to the reference
516
+ * @param options - Update options (byUuid to use UUID lookup, onIdCollision for collision handling)
517
+ * @returns Update result with updated item, success status, and any ID changes
473
518
  */
474
- removeById(id) {
475
- const ref = this.idIndex.get(id);
519
+ async update(identifier, updates, options = {}) {
520
+ const { byUuid = false, ...updateOptions } = options;
521
+ const ref = byUuid ? this.uuidIndex.get(identifier) : this.idIndex.get(identifier);
476
522
  if (!ref) {
477
- return false;
523
+ return { updated: false };
478
524
  }
479
- return this.removeReference(ref);
480
- }
481
- /**
482
- * Find a reference by UUID
483
- */
484
- findByUuid(uuid) {
485
- return this.uuidIndex.get(uuid);
525
+ return this.updateReference(ref, updates, updateOptions);
486
526
  }
487
527
  /**
488
- * Find a reference by ID
528
+ * Find a reference by citation ID or UUID.
529
+ * @param identifier - The citation ID or UUID of the reference to find
530
+ * @param options - Find options (byUuid to use UUID lookup)
531
+ * @returns The CSL item if found, undefined otherwise
489
532
  */
490
- findById(id) {
491
- return this.idIndex.get(id);
533
+ async find(identifier, options = {}) {
534
+ const { byUuid = false } = options;
535
+ const ref = byUuid ? this.uuidIndex.get(identifier) : this.idIndex.get(identifier);
536
+ return ref?.getItem();
492
537
  }
493
538
  /**
494
539
  * Find a reference by DOI
@@ -505,8 +550,8 @@ class Library {
505
550
  /**
506
551
  * Get all references
507
552
  */
508
- getAll() {
509
- return [...this.references];
553
+ async getAll() {
554
+ return this.references.map((ref) => ref.getItem());
510
555
  }
511
556
  /**
512
557
  * Get the file path
@@ -545,281 +590,122 @@ class Library {
545
590
  return false;
546
591
  }
547
592
  this.references.splice(index, 1);
548
- this.uuidIndex.delete(ref.getUuid());
549
- this.idIndex.delete(ref.getId());
550
- const doi = ref.getDoi();
551
- if (doi) {
552
- this.doiIndex.delete(doi);
553
- }
554
- const pmid = ref.getPmid();
555
- if (pmid) {
556
- this.pmidIndex.delete(pmid);
557
- }
593
+ this.removeFromIndices(ref);
558
594
  return true;
559
595
  }
560
- }
561
- const logLevelSchema = z.enum(["silent", "info", "debug"]);
562
- const backupConfigSchema = z.object({
563
- maxGenerations: z.number().int().positive(),
564
- maxAgeDays: z.number().int().positive(),
565
- directory: z.string().min(1)
566
- });
567
- const watchConfigSchema = z.object({
568
- enabled: z.boolean(),
569
- debounceMs: z.number().int().nonnegative(),
570
- pollIntervalMs: z.number().int().positive(),
571
- retryIntervalMs: z.number().int().positive(),
572
- maxRetries: z.number().int().nonnegative()
573
- });
574
- const serverConfigSchema = z.object({
575
- autoStart: z.boolean(),
576
- autoStopMinutes: z.number().int().nonnegative()
577
- });
578
- const configSchema = z.object({
579
- library: z.string().min(1),
580
- logLevel: logLevelSchema,
581
- backup: backupConfigSchema,
582
- watch: watchConfigSchema,
583
- server: serverConfigSchema
584
- });
585
- const partialConfigSchema = z.object({
586
- library: z.string().min(1).optional(),
587
- logLevel: logLevelSchema.optional(),
588
- log_level: logLevelSchema.optional(),
589
- // snake_case support
590
- backup: z.object({
591
- maxGenerations: z.number().int().positive().optional(),
592
- max_generations: z.number().int().positive().optional(),
593
- maxAgeDays: z.number().int().positive().optional(),
594
- max_age_days: z.number().int().positive().optional(),
595
- directory: z.string().min(1).optional()
596
- }).optional(),
597
- watch: z.object({
598
- enabled: z.boolean().optional(),
599
- debounceMs: z.number().int().nonnegative().optional(),
600
- debounce_ms: z.number().int().nonnegative().optional(),
601
- pollIntervalMs: z.number().int().positive().optional(),
602
- poll_interval_ms: z.number().int().positive().optional(),
603
- retryIntervalMs: z.number().int().positive().optional(),
604
- retry_interval_ms: z.number().int().positive().optional(),
605
- maxRetries: z.number().int().nonnegative().optional(),
606
- max_retries: z.number().int().nonnegative().optional()
607
- }).optional(),
608
- server: z.object({
609
- autoStart: z.boolean().optional(),
610
- auto_start: z.boolean().optional(),
611
- autoStopMinutes: z.number().int().nonnegative().optional(),
612
- auto_stop_minutes: z.number().int().nonnegative().optional()
613
- }).optional()
614
- }).passthrough();
615
- function normalizeBackupConfig(backup) {
616
- const normalized = {};
617
- const maxGenerations = backup.maxGenerations ?? backup.max_generations;
618
- if (maxGenerations !== void 0) {
619
- normalized.maxGenerations = maxGenerations;
620
- }
621
- const maxAgeDays = backup.maxAgeDays ?? backup.max_age_days;
622
- if (maxAgeDays !== void 0) {
623
- normalized.maxAgeDays = maxAgeDays;
624
- }
625
- if (backup.directory !== void 0) {
626
- normalized.directory = backup.directory;
627
- }
628
- return Object.keys(normalized).length > 0 ? normalized : void 0;
629
- }
630
- function normalizeWatchConfig(watch) {
631
- const normalized = {};
632
- if (watch.enabled !== void 0) {
633
- normalized.enabled = watch.enabled;
634
- }
635
- const debounceMs = watch.debounceMs ?? watch.debounce_ms;
636
- if (debounceMs !== void 0) {
637
- normalized.debounceMs = debounceMs;
638
- }
639
- const pollIntervalMs = watch.pollIntervalMs ?? watch.poll_interval_ms;
640
- if (pollIntervalMs !== void 0) {
641
- normalized.pollIntervalMs = pollIntervalMs;
642
- }
643
- const retryIntervalMs = watch.retryIntervalMs ?? watch.retry_interval_ms;
644
- if (retryIntervalMs !== void 0) {
645
- normalized.retryIntervalMs = retryIntervalMs;
646
- }
647
- const maxRetries = watch.maxRetries ?? watch.max_retries;
648
- if (maxRetries !== void 0) {
649
- normalized.maxRetries = maxRetries;
650
- }
651
- return Object.keys(normalized).length > 0 ? normalized : void 0;
652
- }
653
- function normalizeServerConfig(server) {
654
- const normalized = {};
655
- const autoStart = server.autoStart ?? server.auto_start;
656
- if (autoStart !== void 0) {
657
- normalized.autoStart = autoStart;
658
- }
659
- const autoStopMinutes = server.autoStopMinutes ?? server.auto_stop_minutes;
660
- if (autoStopMinutes !== void 0) {
661
- normalized.autoStopMinutes = autoStopMinutes;
662
- }
663
- return Object.keys(normalized).length > 0 ? normalized : void 0;
664
- }
665
- function normalizePartialConfig(partial) {
666
- const normalized = {};
667
- if (partial.library !== void 0) {
668
- normalized.library = partial.library;
669
- }
670
- const logLevel = partial.logLevel ?? partial.log_level;
671
- if (logLevel !== void 0) {
672
- normalized.logLevel = logLevel;
673
- }
674
- if (partial.backup !== void 0) {
675
- const backup = normalizeBackupConfig(
676
- partial.backup
677
- );
678
- if (backup) {
679
- normalized.backup = backup;
680
- }
681
- }
682
- if (partial.watch !== void 0) {
683
- const watch = normalizeWatchConfig(partial.watch);
684
- if (watch) {
685
- normalized.watch = watch;
596
+ /**
597
+ * Update a reference with partial updates.
598
+ * Preserves uuid and created_at, updates timestamp.
599
+ */
600
+ updateReference(ref, updates, options = {}) {
601
+ const index = this.references.indexOf(ref);
602
+ if (index === -1) {
603
+ return { updated: false };
686
604
  }
687
- }
688
- if (partial.server !== void 0) {
689
- const server = normalizeServerConfig(
690
- partial.server
605
+ const existingItem = ref.getItem();
606
+ const currentId = ref.getId();
607
+ const { newId, idChanged, collision } = this.resolveNewId(
608
+ updates.id ?? existingItem.id,
609
+ currentId,
610
+ options
691
611
  );
692
- if (server) {
693
- normalized.server = server;
612
+ if (collision) {
613
+ return { updated: false, idCollision: true };
694
614
  }
615
+ const updatedItem = this.buildUpdatedItem(existingItem, updates, newId);
616
+ this.removeFromIndices(ref);
617
+ const newRef = new Reference(updatedItem);
618
+ this.references[index] = newRef;
619
+ this.addToIndices(newRef);
620
+ const result = { updated: true, item: newRef.getItem() };
621
+ if (idChanged) {
622
+ result.idChanged = true;
623
+ result.newId = newId;
624
+ }
625
+ return result;
695
626
  }
696
- return normalized;
697
- }
698
- function getDefaultBackupDirectory() {
699
- return join(tmpdir(), "reference-manager", "backups");
700
- }
701
- function getDefaultLibraryPath() {
702
- return join(homedir(), ".reference-manager", "csl.library.json");
703
- }
704
- function getDefaultUserConfigPath() {
705
- return join(homedir(), ".reference-manager", "config.toml");
706
- }
707
- function getDefaultCurrentDirConfigFilename() {
708
- return ".reference-manager.config.toml";
709
- }
710
- const defaultConfig = {
711
- library: getDefaultLibraryPath(),
712
- logLevel: "info",
713
- backup: {
714
- maxGenerations: 50,
715
- maxAgeDays: 365,
716
- directory: getDefaultBackupDirectory()
717
- },
718
- watch: {
719
- enabled: true,
720
- debounceMs: 500,
721
- pollIntervalMs: 5e3,
722
- retryIntervalMs: 200,
723
- maxRetries: 10
724
- },
725
- server: {
726
- autoStart: false,
727
- autoStopMinutes: 0
728
- }
729
- };
730
- function loadTOMLFile(path) {
731
- if (!existsSync(path)) {
732
- return null;
733
- }
734
- try {
735
- const content = readFileSync(path, "utf-8");
736
- const parsed = parse(content);
737
- const validated = partialConfigSchema.parse(parsed);
738
- return validated;
739
- } catch (error) {
740
- throw new Error(
741
- `Failed to load config from ${path}: ${error instanceof Error ? error.message : String(error)}`
742
- );
743
- }
744
- }
745
- function mergeConfigs(base, ...overrides) {
746
- const result = { ...base };
747
- for (const override of overrides) {
748
- if (!override) continue;
749
- if (override.library !== void 0) {
750
- result.library = override.library;
627
+ /**
628
+ * Resolve the new ID, handling collisions based on options.
629
+ */
630
+ resolveNewId(requestedId, currentId, options) {
631
+ if (requestedId === currentId) {
632
+ return { newId: requestedId, idChanged: false, collision: false };
751
633
  }
752
- if (override.logLevel !== void 0) {
753
- result.logLevel = override.logLevel;
634
+ const conflictingRef = this.idIndex.get(requestedId);
635
+ if (!conflictingRef) {
636
+ return { newId: requestedId, idChanged: false, collision: false };
754
637
  }
755
- if (override.backup !== void 0) {
756
- result.backup = {
757
- ...result.backup,
758
- ...override.backup
759
- };
638
+ const onIdCollision = options.onIdCollision ?? "fail";
639
+ if (onIdCollision === "fail") {
640
+ return { newId: requestedId, idChanged: false, collision: true };
760
641
  }
761
- if (override.watch !== void 0) {
762
- result.watch = {
763
- ...result.watch,
764
- ...override.watch
765
- };
642
+ const existingIds = new Set(this.references.map((r) => r.getId()));
643
+ existingIds.delete(currentId);
644
+ const resolvedId = this.resolveIdCollision(requestedId, existingIds);
645
+ return { newId: resolvedId, idChanged: true, collision: false };
646
+ }
647
+ /**
648
+ * Build the updated CslItem, preserving uuid and created_at.
649
+ */
650
+ buildUpdatedItem(existingItem, updates, newId) {
651
+ return {
652
+ ...existingItem,
653
+ ...updates,
654
+ id: newId,
655
+ type: updates.type ?? existingItem.type,
656
+ custom: {
657
+ ...existingItem.custom || {},
658
+ ...updates.custom || {},
659
+ uuid: existingItem.custom?.uuid || "",
660
+ created_at: existingItem.custom?.created_at || (/* @__PURE__ */ new Date()).toISOString(),
661
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
662
+ }
663
+ };
664
+ }
665
+ /**
666
+ * Remove a reference from all indices.
667
+ */
668
+ removeFromIndices(ref) {
669
+ this.uuidIndex.delete(ref.getUuid());
670
+ this.idIndex.delete(ref.getId());
671
+ const doi = ref.getDoi();
672
+ if (doi) {
673
+ this.doiIndex.delete(doi);
766
674
  }
767
- if (override.server !== void 0) {
768
- result.server = {
769
- ...result.server,
770
- ...override.server
771
- };
675
+ const pmid = ref.getPmid();
676
+ if (pmid) {
677
+ this.pmidIndex.delete(pmid);
772
678
  }
773
679
  }
774
- return result;
775
- }
776
- function fillDefaults(partial) {
777
- return {
778
- library: partial.library ?? defaultConfig.library,
779
- logLevel: partial.logLevel ?? defaultConfig.logLevel,
780
- backup: {
781
- maxGenerations: partial.backup?.maxGenerations ?? defaultConfig.backup.maxGenerations,
782
- maxAgeDays: partial.backup?.maxAgeDays ?? defaultConfig.backup.maxAgeDays,
783
- directory: partial.backup?.directory ?? defaultConfig.backup.directory
784
- },
785
- watch: {
786
- enabled: partial.watch?.enabled ?? defaultConfig.watch.enabled,
787
- debounceMs: partial.watch?.debounceMs ?? defaultConfig.watch.debounceMs,
788
- pollIntervalMs: partial.watch?.pollIntervalMs ?? defaultConfig.watch.pollIntervalMs,
789
- retryIntervalMs: partial.watch?.retryIntervalMs ?? defaultConfig.watch.retryIntervalMs,
790
- maxRetries: partial.watch?.maxRetries ?? defaultConfig.watch.maxRetries
791
- },
792
- server: {
793
- autoStart: partial.server?.autoStart ?? defaultConfig.server.autoStart,
794
- autoStopMinutes: partial.server?.autoStopMinutes ?? defaultConfig.server.autoStopMinutes
680
+ /**
681
+ * Generate an alphabetic suffix for ID collision resolution.
682
+ * 0 -> 'a', 1 -> 'b', ..., 25 -> 'z', 26 -> 'aa', etc.
683
+ */
684
+ generateSuffix(index) {
685
+ const alphabet = "abcdefghijklmnopqrstuvwxyz";
686
+ let suffix = "";
687
+ let n = index;
688
+ do {
689
+ suffix = alphabet[n % 26] + suffix;
690
+ n = Math.floor(n / 26) - 1;
691
+ } while (n >= 0);
692
+ return suffix;
693
+ }
694
+ /**
695
+ * Resolve ID collision by appending alphabetic suffix.
696
+ */
697
+ resolveIdCollision(baseId, existingIds) {
698
+ if (!existingIds.has(baseId)) {
699
+ return baseId;
795
700
  }
796
- };
797
- }
798
- function loadConfig(options = {}) {
799
- const cwd = options.cwd ?? process.cwd();
800
- const userConfigPath = options.userConfigPath ?? getDefaultUserConfigPath();
801
- const userConfig = loadTOMLFile(userConfigPath);
802
- const envConfigPath = process.env.REFERENCE_MANAGER_CONFIG;
803
- const envConfig = envConfigPath ? loadTOMLFile(envConfigPath) : null;
804
- const currentConfigPath = join(cwd, getDefaultCurrentDirConfigFilename());
805
- const currentConfig = loadTOMLFile(currentConfigPath);
806
- const normalizedUser = userConfig ? normalizePartialConfig(userConfig) : null;
807
- const normalizedEnv = envConfig ? normalizePartialConfig(envConfig) : null;
808
- const normalizedCurrent = currentConfig ? normalizePartialConfig(currentConfig) : null;
809
- const merged = mergeConfigs(
810
- {},
811
- normalizedUser,
812
- normalizedEnv,
813
- normalizedCurrent,
814
- options.overrides
815
- );
816
- const config = fillDefaults(merged);
817
- try {
818
- return configSchema.parse(config);
819
- } catch (error) {
820
- throw new Error(
821
- `Invalid configuration: ${error instanceof Error ? error.message : String(error)}`
822
- );
701
+ let index = 0;
702
+ let newId;
703
+ do {
704
+ const suffix = this.generateSuffix(index);
705
+ newId = `${baseId}${suffix}`;
706
+ index++;
707
+ } while (existingIds.has(newId));
708
+ return newId;
823
709
  }
824
710
  }
825
711
  const VALID_FIELDS = /* @__PURE__ */ new Set([
@@ -830,7 +716,8 @@ const VALID_FIELDS = /* @__PURE__ */ new Set([
830
716
  "pmid",
831
717
  "pmcid",
832
718
  "url",
833
- "keyword"
719
+ "keyword",
720
+ "tag"
834
721
  ]);
835
722
  function isWhitespace(query, index) {
836
723
  return /\s/.test(query.charAt(index));
@@ -993,6 +880,82 @@ function normalize(text) {
993
880
  normalized = normalized.replace(/\s+/g, " ").trim();
994
881
  return normalized;
995
882
  }
883
+ function normalizePreservingCase(text) {
884
+ let normalized = text.normalize("NFKC");
885
+ normalized = normalized.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "");
886
+ normalized = normalized.replace(/\s+/g, " ").trim();
887
+ return normalized;
888
+ }
889
+ function hasConsecutiveUppercase(text) {
890
+ const pattern = /[A-Z]{2,}/;
891
+ return pattern.test(text);
892
+ }
893
+ function extractUppercaseSegments(text) {
894
+ const pattern = /[A-Z]{2,}/g;
895
+ const segments = [];
896
+ for (const match of text.matchAll(pattern)) {
897
+ segments.push({
898
+ segment: match[0],
899
+ start: match.index,
900
+ end: match.index + match[0].length
901
+ });
902
+ }
903
+ return segments;
904
+ }
905
+ function escapeRegex(str) {
906
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
907
+ }
908
+ function normalizeWhitespace(text) {
909
+ return text.replace(/\s+/g, " ").trim();
910
+ }
911
+ function allUppercaseSegmentsExist(segments, target) {
912
+ return segments.every((seg) => target.includes(seg.segment));
913
+ }
914
+ function buildMatchPattern(query, segments) {
915
+ const patternParts = [];
916
+ let lastEnd = 0;
917
+ for (const seg of segments) {
918
+ if (seg.start > lastEnd) {
919
+ const beforePart = query.slice(lastEnd, seg.start);
920
+ if (beforePart.trim()) {
921
+ patternParts.push(escapeRegex(beforePart));
922
+ }
923
+ }
924
+ patternParts.push(`(?:${escapeRegex(seg.segment)})`);
925
+ lastEnd = seg.end;
926
+ }
927
+ if (lastEnd < query.length) {
928
+ const afterPart = query.slice(lastEnd);
929
+ if (afterPart.trim()) {
930
+ patternParts.push(escapeRegex(afterPart));
931
+ }
932
+ }
933
+ return patternParts.join(".*?");
934
+ }
935
+ function matchWithUppercaseSensitivity(query, target) {
936
+ if (query === "") {
937
+ return true;
938
+ }
939
+ if (target === "") {
940
+ return false;
941
+ }
942
+ const normalizedQuery = normalizeWhitespace(query);
943
+ const normalizedTarget = normalizeWhitespace(target);
944
+ if (!hasConsecutiveUppercase(normalizedQuery)) {
945
+ return normalizedTarget.toLowerCase().includes(normalizedQuery.toLowerCase());
946
+ }
947
+ const segments = extractUppercaseSegments(normalizedQuery);
948
+ if (!allUppercaseSegmentsExist(segments, target)) {
949
+ return false;
950
+ }
951
+ const pattern = buildMatchPattern(normalizedQuery, segments);
952
+ try {
953
+ const regex = new RegExp(pattern, "i");
954
+ return regex.test(normalizedTarget);
955
+ } catch {
956
+ return normalizedTarget.toLowerCase().includes(normalizedQuery.toLowerCase());
957
+ }
958
+ }
996
959
  const ID_FIELDS = /* @__PURE__ */ new Set(["DOI", "PMID", "PMCID", "URL"]);
997
960
  function extractYear$2(reference) {
998
961
  if (reference.issued?.["date-parts"]?.[0]?.[0]) {
@@ -1006,8 +969,8 @@ function extractAuthors(reference) {
1006
969
  }
1007
970
  return reference.author.map((author) => {
1008
971
  const family = author.family || "";
1009
- const givenInitial = author.given ? author.given[0] : "";
1010
- return givenInitial ? `${family} ${givenInitial}` : family;
972
+ const given = author.given || "";
973
+ return given ? `${family} ${given}` : family;
1011
974
  }).join(" ");
1012
975
  }
1013
976
  function getFieldValue(reference, field) {
@@ -1056,11 +1019,11 @@ function matchKeyword(queryValue, reference) {
1056
1019
  if (!reference.keyword || !Array.isArray(reference.keyword)) {
1057
1020
  return null;
1058
1021
  }
1059
- const normalizedQuery = normalize(queryValue);
1022
+ const normalizedQuery = normalizePreservingCase(queryValue);
1060
1023
  for (const keyword of reference.keyword) {
1061
1024
  if (typeof keyword === "string") {
1062
- const normalizedKeyword = normalize(keyword);
1063
- if (normalizedKeyword.includes(normalizedQuery)) {
1025
+ const normalizedKeyword = normalizePreservingCase(keyword);
1026
+ if (matchWithUppercaseSensitivity(normalizedQuery, normalizedKeyword)) {
1064
1027
  return {
1065
1028
  field: "keyword",
1066
1029
  strength: "partial",
@@ -1071,6 +1034,25 @@ function matchKeyword(queryValue, reference) {
1071
1034
  }
1072
1035
  return null;
1073
1036
  }
1037
+ function matchTag(queryValue, reference) {
1038
+ if (!reference.custom?.tags || !Array.isArray(reference.custom.tags)) {
1039
+ return null;
1040
+ }
1041
+ const normalizedQuery = normalizePreservingCase(queryValue);
1042
+ for (const tag of reference.custom.tags) {
1043
+ if (typeof tag === "string") {
1044
+ const normalizedTag = normalizePreservingCase(tag);
1045
+ if (matchWithUppercaseSensitivity(normalizedQuery, normalizedTag)) {
1046
+ return {
1047
+ field: "tag",
1048
+ strength: "partial",
1049
+ value: tag
1050
+ };
1051
+ }
1052
+ }
1053
+ }
1054
+ return null;
1055
+ }
1074
1056
  const FIELD_MAP = {
1075
1057
  author: "author",
1076
1058
  title: "title",
@@ -1104,9 +1086,9 @@ function matchFieldValue(field, tokenValue, reference) {
1104
1086
  }
1105
1087
  return null;
1106
1088
  }
1107
- const normalizedFieldValue = normalize(fieldValue);
1108
- const normalizedQuery = normalize(tokenValue);
1109
- if (normalizedFieldValue.includes(normalizedQuery)) {
1089
+ const normalizedFieldValue = normalizePreservingCase(fieldValue);
1090
+ const normalizedQuery = normalizePreservingCase(tokenValue);
1091
+ if (matchWithUppercaseSensitivity(normalizedQuery, normalizedFieldValue)) {
1110
1092
  return {
1111
1093
  field,
1112
1094
  strength: "partial",
@@ -1133,6 +1115,11 @@ function matchSpecificField(token, reference) {
1133
1115
  if (keywordMatch) matches.push(keywordMatch);
1134
1116
  return matches;
1135
1117
  }
1118
+ if (fieldToSearch === "tag") {
1119
+ const tagMatch = matchTag(token.value, reference);
1120
+ if (tagMatch) matches.push(tagMatch);
1121
+ return matches;
1122
+ }
1136
1123
  const actualField = FIELD_MAP[fieldToSearch] || fieldToSearch;
1137
1124
  const match = matchFieldValue(actualField, token.value, reference);
1138
1125
  if (match) matches.push(match);
@@ -1158,11 +1145,14 @@ function matchSingleField(field, tokenValue, reference) {
1158
1145
  if (field === "keyword") {
1159
1146
  return matchKeyword(tokenValue, reference);
1160
1147
  }
1148
+ if (field === "tag") {
1149
+ return matchTag(tokenValue, reference);
1150
+ }
1161
1151
  return matchFieldValue(field, tokenValue, reference);
1162
1152
  }
1163
1153
  function matchAllFields(token, reference) {
1164
1154
  const matches = [];
1165
- const specialFields = ["year", "URL", "keyword"];
1155
+ const specialFields = ["year", "URL", "keyword", "tag"];
1166
1156
  for (const field of specialFields) {
1167
1157
  const match = matchSingleField(field, token.value, reference);
1168
1158
  if (match) matches.push(match);
@@ -1377,39 +1367,191 @@ function detectDuplicate(item, existingReferences) {
1377
1367
  matches
1378
1368
  };
1379
1369
  }
1370
+ const DEFAULT_DEBOUNCE_MS = 500;
1371
+ const DEFAULT_POLL_INTERVAL_MS = 5e3;
1372
+ const DEFAULT_RETRY_DELAY_MS = 200;
1373
+ const DEFAULT_MAX_RETRIES = 10;
1374
+ function shouldIgnore(filePath) {
1375
+ const basename = path.basename(filePath);
1376
+ if (basename.endsWith(".tmp")) return true;
1377
+ if (basename.endsWith(".bak")) return true;
1378
+ if (basename.includes(".conflict.")) return true;
1379
+ if (basename.endsWith(".lock")) return true;
1380
+ if (basename.startsWith(".") && basename.endsWith(".swp")) return true;
1381
+ if (basename.endsWith("~")) return true;
1382
+ return false;
1383
+ }
1384
+ class FileWatcher extends EventEmitter {
1385
+ watchPath;
1386
+ debounceMs;
1387
+ pollIntervalMs;
1388
+ usePolling;
1389
+ retryDelayMs;
1390
+ maxRetries;
1391
+ watcher = null;
1392
+ watching = false;
1393
+ debounceTimers = /* @__PURE__ */ new Map();
1394
+ constructor(watchPath, options) {
1395
+ super();
1396
+ this.watchPath = watchPath;
1397
+ this.debounceMs = options?.debounceMs ?? DEFAULT_DEBOUNCE_MS;
1398
+ this.pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
1399
+ this.usePolling = options?.usePolling ?? false;
1400
+ this.retryDelayMs = options?.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
1401
+ this.maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
1402
+ }
1403
+ /**
1404
+ * Start watching for file changes
1405
+ */
1406
+ async start() {
1407
+ if (this.watching) {
1408
+ return;
1409
+ }
1410
+ return new Promise((resolve, reject) => {
1411
+ this.watcher = chokidar.watch(this.watchPath, {
1412
+ ignored: shouldIgnore,
1413
+ persistent: true,
1414
+ usePolling: this.usePolling,
1415
+ interval: this.pollIntervalMs,
1416
+ ignoreInitial: true,
1417
+ awaitWriteFinish: false
1418
+ });
1419
+ this.watcher.on("ready", () => {
1420
+ this.watching = true;
1421
+ this.emit("ready");
1422
+ resolve();
1423
+ });
1424
+ this.watcher.on("error", (error) => {
1425
+ this.emit("error", error);
1426
+ if (!this.watching) {
1427
+ reject(error);
1428
+ }
1429
+ });
1430
+ this.watcher.on("change", (filePath) => {
1431
+ this.handleFileChange(filePath);
1432
+ });
1433
+ this.watcher.on("add", (filePath) => {
1434
+ this.handleFileChange(filePath);
1435
+ });
1436
+ });
1437
+ }
1438
+ /**
1439
+ * Handle file change with debouncing
1440
+ */
1441
+ handleFileChange(filePath) {
1442
+ const existingTimer = this.debounceTimers.get(filePath);
1443
+ if (existingTimer) {
1444
+ clearTimeout(existingTimer);
1445
+ }
1446
+ const timer = setTimeout(() => {
1447
+ this.debounceTimers.delete(filePath);
1448
+ this.emit("change", filePath);
1449
+ this.tryParseJsonFile(filePath);
1450
+ }, this.debounceMs);
1451
+ this.debounceTimers.set(filePath, timer);
1452
+ }
1453
+ /**
1454
+ * Try to parse JSON file with retries
1455
+ */
1456
+ async tryParseJsonFile(filePath) {
1457
+ if (path.extname(filePath).toLowerCase() !== ".json") {
1458
+ return;
1459
+ }
1460
+ let lastError = null;
1461
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
1462
+ try {
1463
+ const content = await fs.readFile(filePath, "utf-8");
1464
+ const parsed = JSON.parse(content);
1465
+ this.emit("parsed", filePath, parsed);
1466
+ return;
1467
+ } catch (error) {
1468
+ lastError = error;
1469
+ if (attempt < this.maxRetries) {
1470
+ await this.delay(this.retryDelayMs);
1471
+ }
1472
+ }
1473
+ }
1474
+ this.emit("parseError", filePath, lastError);
1475
+ }
1476
+ /**
1477
+ * Delay helper
1478
+ */
1479
+ delay(ms) {
1480
+ return new Promise((resolve) => setTimeout(resolve, ms));
1481
+ }
1482
+ /**
1483
+ * Stop watching for file changes
1484
+ */
1485
+ close() {
1486
+ if (this.watcher) {
1487
+ this.watcher.close();
1488
+ this.watcher = null;
1489
+ }
1490
+ for (const timer of this.debounceTimers.values()) {
1491
+ clearTimeout(timer);
1492
+ }
1493
+ this.debounceTimers.clear();
1494
+ this.watching = false;
1495
+ }
1496
+ /**
1497
+ * Get the watched path
1498
+ */
1499
+ getPath() {
1500
+ return this.watchPath;
1501
+ }
1502
+ /**
1503
+ * Check if the watcher is currently active
1504
+ */
1505
+ isWatching() {
1506
+ return this.watching;
1507
+ }
1508
+ /**
1509
+ * Get the debounce time in milliseconds
1510
+ */
1511
+ getDebounceMs() {
1512
+ return this.debounceMs;
1513
+ }
1514
+ /**
1515
+ * Get the poll interval in milliseconds
1516
+ */
1517
+ getPollIntervalMs() {
1518
+ return this.pollIntervalMs;
1519
+ }
1520
+ /**
1521
+ * Get the retry delay in milliseconds
1522
+ */
1523
+ getRetryDelayMs() {
1524
+ return this.retryDelayMs;
1525
+ }
1526
+ /**
1527
+ * Get the maximum number of retries
1528
+ */
1529
+ getMaxRetries() {
1530
+ return this.maxRetries;
1531
+ }
1532
+ }
1380
1533
  export {
1381
- generateUuid as A,
1382
- isValidUuid as B,
1383
1534
  CslLibrarySchema as C,
1384
- ensureCustomMetadata as D,
1385
- extractUuidFromCustom as E,
1535
+ FileWatcher as F,
1386
1536
  Library as L,
1387
1537
  Reference as R,
1388
1538
  computeHash as a,
1389
- backupConfigSchema as b,
1539
+ sortResults as b,
1390
1540
  computeFileHash as c,
1391
- configSchema as d,
1392
- defaultConfig as e,
1393
- getDefaultCurrentDirConfigFilename as f,
1394
- getDefaultBackupDirectory as g,
1395
- getDefaultLibraryPath as h,
1396
- getDefaultUserConfigPath as i,
1397
- logLevelSchema as j,
1398
- normalize as k,
1399
- loadConfig as l,
1400
- sortResults as m,
1401
- normalizePartialConfig as n,
1402
- detectDuplicate as o,
1403
- partialConfigSchema as p,
1404
- CslItemSchema as q,
1405
- parseCslJson as r,
1541
+ detectDuplicate as d,
1542
+ CslItemSchema as e,
1543
+ serializeCslJson as f,
1544
+ generateId as g,
1545
+ generateIdWithCollisionCheck as h,
1546
+ normalizeText as i,
1547
+ generateUuid as j,
1548
+ isValidUuid as k,
1549
+ ensureCustomMetadata as l,
1550
+ extractUuidFromCustom as m,
1551
+ normalize as n,
1552
+ parseCslJson as p,
1406
1553
  search as s,
1407
1554
  tokenize as t,
1408
- serializeCslJson as u,
1409
- writeCslJson as v,
1410
- watchConfigSchema as w,
1411
- generateId as x,
1412
- generateIdWithCollisionCheck as y,
1413
- normalizeText as z
1555
+ writeCslJson as w
1414
1556
  };
1415
- //# sourceMappingURL=detector-BF8Mcc72.js.map
1557
+ //# sourceMappingURL=file-watcher-B-SiUw5f.js.map