citeclaw 2.0.1 → 2.0.3

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 (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/botcite.js +98 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "citeclaw",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Citation and bibliography toolkit for DOI, URL, arXiv, PDF, Zotero, and MCP workflows.",
5
5
  "homepage": "https://github.com/trotsky1997/citeclaw",
6
6
  "license": "Apache-2.0",
@@ -28,13 +28,45 @@ const vendoredOfficialStylesDir = path.join( rootDir, 'vendor', 'styles-official
28
28
  const localDir = path.join( rootDir, '.local' );
29
29
  const logDir = path.join( localDir, 'logs' );
30
30
  const stateDir = path.join( localDir, 'state' );
31
+ const localTranslatorSourcesDir = path.join( localDir, 'translator-sources' );
31
32
  const mergedTranslatorsDir = process.env.LOCAL_TRANSLATORS_DIR ||
32
33
  path.join( localDir, 'translators' );
33
34
  const stylesRootDir = process.env.LOCAL_STYLES_DIR ||
34
35
  path.join( localDir, 'styles' );
36
+ const localStyleSourcesDir = path.join( localDir, 'style-sources' );
35
37
  const cslDir = path.join( stylesRootDir, 'csl' );
36
38
  const localeDir = path.join( stylesRootDir, 'locales' );
37
39
  const defaultStyleSources = [ vendoredOfficialStylesDir, vendoredStylesDir ];
40
+ const defaultStyleGitSources = [
41
+ {
42
+ url: 'https://github.com/citation-style-language/styles.git',
43
+ dir: path.join( localStyleSourcesDir, 'styles-official' )
44
+ },
45
+ {
46
+ url: 'https://github.com/zotero-chinese/styles.git',
47
+ dir: path.join( localStyleSourcesDir, 'styles' )
48
+ }
49
+ ];
50
+ const defaultTranslatorGitSources = [
51
+ {
52
+ url: 'https://github.com/zotero/translators.git',
53
+ dir: path.join( localTranslatorSourcesDir, 'translators-official' )
54
+ },
55
+ {
56
+ url: 'https://github.com/l0o0/translators_CN.git',
57
+ dir: path.join( localTranslatorSourcesDir, 'translators_CN' )
58
+ }
59
+ ];
60
+
61
+ function resolvedOfficialTranslatorsDir() {
62
+ const fallback = defaultTranslatorGitSources[ 0 ].dir;
63
+ return fileExists( officialTranslatorsDir ) ? officialTranslatorsDir : fallback;
64
+ }
65
+
66
+ function resolvedCnTranslatorsDir() {
67
+ const fallback = defaultTranslatorGitSources[ 1 ].dir;
68
+ return fileExists( cnTranslatorsDir ) ? cnTranslatorsDir : fallback;
69
+ }
38
70
  const defaultPdfFetchIntervalMs = parseInt( process.env.CITOID_LOCAL_FETCH_INTERVAL_MS || '800', 10 );
39
71
  const defaultRequestTimeoutMs = parseInt( process.env.CITOID_LOCAL_FETCH_TIMEOUT_MS || '15000', 10 );
40
72
  const defaultProbeBodyBytes = parseInt( process.env.CITOID_LOCAL_PROBE_BODY_BYTES || '1572864', 10 );
@@ -469,13 +501,15 @@ function repoHeadOrMissing( repoPath ) {
469
501
 
470
502
  function translatorsNeedSync() {
471
503
  const stampPath = path.join( stateDir, 'translators-sync.stamp' );
504
+ const activeOfficialDir = resolvedOfficialTranslatorsDir();
505
+ const activeCnDir = resolvedCnTranslatorsDir();
472
506
  const hasMerged = fileExists( mergedTranslatorsDir ) &&
473
507
  fs.readdirSync( mergedTranslatorsDir ).some( ( name ) => name.endsWith( '.js' ) );
474
508
  if ( !hasMerged ) {
475
509
  return true;
476
510
  }
477
511
 
478
- const currentStamp = `zotero=${ repoHeadOrMissing( path.join( zoteroDir, 'modules', 'translators' ) ) };official=${ repoHeadOrMissing( officialTranslatorsDir ) };cn=${ repoHeadOrMissing( cnTranslatorsDir ) }`;
512
+ const currentStamp = `zotero=${ repoHeadOrMissing( path.join( zoteroDir, 'modules', 'translators' ) ) };official=${ repoHeadOrMissing( activeOfficialDir ) };cn=${ repoHeadOrMissing( activeCnDir ) }`;
479
513
  let previousStamp = '';
480
514
  if ( fileExists( stampPath ) ) {
481
515
  previousStamp = fs.readFileSync( stampPath, 'utf8' ).trim();
@@ -485,7 +519,7 @@ function translatorsNeedSync() {
485
519
 
486
520
  function writeTranslatorStamp() {
487
521
  const stampPath = path.join( stateDir, 'translators-sync.stamp' );
488
- const stamp = `zotero=${ repoHeadOrMissing( path.join( zoteroDir, 'modules', 'translators' ) ) };official=${ repoHeadOrMissing( officialTranslatorsDir ) };cn=${ repoHeadOrMissing( cnTranslatorsDir ) }`;
522
+ const stamp = `zotero=${ repoHeadOrMissing( path.join( zoteroDir, 'modules', 'translators' ) ) };official=${ repoHeadOrMissing( resolvedOfficialTranslatorsDir() ) };cn=${ repoHeadOrMissing( resolvedCnTranslatorsDir() ) }`;
489
523
  fs.writeFileSync( stampPath, `${ stamp }\n` );
490
524
  }
491
525
 
@@ -493,6 +527,8 @@ function syncMergedTranslators() {
493
527
  fs.rmSync( mergedTranslatorsDir, { recursive: true, force: true } );
494
528
  fs.mkdirSync( mergedTranslatorsDir, { recursive: true } );
495
529
  const zoteroTranslators = path.join( zoteroDir, 'modules', 'translators' );
530
+ const activeOfficialDir = resolvedOfficialTranslatorsDir();
531
+ const activeCnDir = resolvedCnTranslatorsDir();
496
532
  if ( fileExists( zoteroTranslators ) ) {
497
533
  fs.readdirSync( zoteroTranslators )
498
534
  .filter( ( name ) => name.endsWith( '.js' ) )
@@ -503,22 +539,22 @@ function syncMergedTranslators() {
503
539
  );
504
540
  } );
505
541
  }
506
- if ( fileExists( officialTranslatorsDir ) ) {
507
- fs.readdirSync( officialTranslatorsDir )
542
+ if ( fileExists( activeOfficialDir ) ) {
543
+ fs.readdirSync( activeOfficialDir )
508
544
  .filter( ( name ) => name.endsWith( '.js' ) )
509
545
  .forEach( ( name ) => {
510
546
  fs.copyFileSync(
511
- path.join( officialTranslatorsDir, name ),
547
+ path.join( activeOfficialDir, name ),
512
548
  path.join( mergedTranslatorsDir, name )
513
549
  );
514
550
  } );
515
551
  }
516
- if ( fileExists( cnTranslatorsDir ) ) {
517
- fs.readdirSync( cnTranslatorsDir )
552
+ if ( fileExists( activeCnDir ) ) {
553
+ fs.readdirSync( activeCnDir )
518
554
  .filter( ( name ) => name.endsWith( '.js' ) )
519
555
  .forEach( ( name ) => {
520
556
  fs.copyFileSync(
521
- path.join( cnTranslatorsDir, name ),
557
+ path.join( activeCnDir, name ),
522
558
  path.join( mergedTranslatorsDir, name )
523
559
  );
524
560
  } );
@@ -526,10 +562,39 @@ function syncMergedTranslators() {
526
562
  writeTranslatorStamp();
527
563
  }
528
564
 
565
+ function ensureDefaultTranslatorSources() {
566
+ const existingOfficial = fileExists( officialTranslatorsDir ) ? officialTranslatorsDir : null;
567
+ const existingCn = fileExists( cnTranslatorsDir ) ? cnTranslatorsDir : null;
568
+ if ( existingOfficial && existingCn ) {
569
+ return {
570
+ officialDir: existingOfficial,
571
+ cnDir: existingCn
572
+ };
573
+ }
574
+ if ( !commandExists( 'git' ) ) {
575
+ throw new Error(
576
+ 'missing translator sources and git is not installed; install git or provide vendored translator directories'
577
+ );
578
+ }
579
+ fs.mkdirSync( localTranslatorSourcesDir, { recursive: true } );
580
+ defaultTranslatorGitSources.forEach( ( source ) => {
581
+ if ( fileExists( source.dir ) ) {
582
+ runCommandOrThrow( 'git', [ '-C', source.dir, 'pull', '--ff-only' ] );
583
+ return;
584
+ }
585
+ runCommandOrThrow( 'git', [ 'clone', '--depth', '1', source.url, source.dir ] );
586
+ } );
587
+ return {
588
+ officialDir: defaultTranslatorGitSources[ 0 ].dir,
589
+ cnDir: defaultTranslatorGitSources[ 1 ].dir
590
+ };
591
+ }
592
+
529
593
  function bootstrapLocalEnvironment() {
530
594
  ensureDirs();
531
595
  const installer = resolveInstallCommand();
532
- if ( !repoReady( zoteroDir ) || !repoReady( cnTranslatorsDir ) || !repoReady( officialTranslatorsDir ) ) {
596
+ const translatorSources = ensureDefaultTranslatorSources();
597
+ if ( !repoReady( zoteroDir ) || !repoReady( translatorSources.cnDir ) || !repoReady( translatorSources.officialDir ) ) {
533
598
  throw new Error(
534
599
  'missing vendored repos under vendor/. pull the complete repository content.'
535
600
  );
@@ -572,6 +637,27 @@ function ensureStyleRuntime() {
572
637
  throw new Error( 'Styles are not synced. Run: citeclaw styles sync' );
573
638
  }
574
639
 
640
+ function ensureDefaultStyleSources() {
641
+ const existing = defaultStyleSources.filter( ( sourceDir ) => fileExists( sourceDir ) );
642
+ if ( existing.length ) {
643
+ return existing;
644
+ }
645
+ if ( !commandExists( 'git' ) ) {
646
+ throw new Error(
647
+ 'no bundled style sources found in this package, and git is not installed; install git or pass --repo <styles-dir>'
648
+ );
649
+ }
650
+ fs.mkdirSync( localStyleSourcesDir, { recursive: true } );
651
+ defaultStyleGitSources.forEach( ( source ) => {
652
+ if ( fileExists( source.dir ) ) {
653
+ runCommandOrThrow( 'git', [ '-C', source.dir, 'pull', '--ff-only' ] );
654
+ return;
655
+ }
656
+ runCommandOrThrow( 'git', [ 'clone', '--depth', '1', source.url, source.dir ] );
657
+ } );
658
+ return defaultStyleGitSources.map( ( source ) => source.dir );
659
+ }
660
+
575
661
  function walkFiles( dirPath ) {
576
662
  const entries = fs.readdirSync( dirPath, { withFileTypes: true } );
577
663
  let result = [];
@@ -1393,16 +1479,11 @@ async function syncStyles( options ) {
1393
1479
  options.repo :
1394
1480
  path.join( rootDir, options.repo || 'vendor/styles' )
1395
1481
  ] :
1396
- defaultStyleSources;
1482
+ ensureDefaultStyleSources();
1397
1483
  const sourceDirs = configuredSourceDirs.filter( ( sourceDir ) => fileExists( sourceDir ) );
1398
1484
  if ( options.repo && !sourceDirs.length ) {
1399
1485
  throw new Error( `styles source not found: ${ configuredSourceDirs.join( ', ' ) }` );
1400
1486
  }
1401
- if ( !options.repo && !sourceDirs.length ) {
1402
- throw new Error(
1403
- 'no bundled style sources found in this package; provide --repo <styles-dir> or run from a full source checkout'
1404
- );
1405
- }
1406
1487
 
1407
1488
  let copiedCount = 0;
1408
1489
  sourceDirs.forEach( ( sourceDir ) => {
@@ -1578,7 +1659,7 @@ function isHttpUrl( raw ) {
1578
1659
  function normalizeArxivId( raw ) {
1579
1660
  const value = String( raw || '' ).trim();
1580
1661
  const id = value
1581
- .replace( /^https?:\/\/arxiv\.org\/(?:abs|pdf)\//i, '' )
1662
+ .replace( /^https?:\/\/arxiv\.org\/(?:abs|pdf|html)\//i, '' )
1582
1663
  .replace( /\.pdf$/i, '' )
1583
1664
  .replace( /^arxiv:/i, '' )
1584
1665
  .trim();
@@ -5012,6 +5093,7 @@ module.exports = {
5012
5093
  extractBestDoiCandidate,
5013
5094
  extractPdfCandidates,
5014
5095
  main,
5096
+ normalizeArxivId,
5015
5097
  normalizeDoi,
5016
5098
  shouldAttemptPdfOcr
5017
5099
  };