citeclaw 2.0.2 → 2.0.4

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 +126 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "citeclaw",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
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 );
@@ -80,6 +112,7 @@ function usage() {
80
112
  console.error( ' citeclaw cite-pdf [--headers] [--debug-pdf] <pdf-path>' );
81
113
  console.error( ' citeclaw fetch-pdf [--base <openurl-base>] [--out <file.pdf>] <doi|arxiv|url>' );
82
114
  console.error( ' citeclaw openurl-resolve [--base <openurl-base>] <doi|arxiv|url>' );
115
+ console.error( ' citeclaw translators sync' );
83
116
  console.error( ' citeclaw zotero <login|logout|whoami|query|dump|cite|add|delete|update|note|sync-cite|dedup|enrich|export|watch|templates|safe-mode> [...]' );
84
117
  console.error( ' citeclaw batch --op <cite|cite-style|fetch-pdf|openurl-resolve> --in <file>' );
85
118
  console.error( ' citeclaw styles sync [--repo <git-url>]' );
@@ -106,6 +139,7 @@ function usage() {
106
139
  console.error( " citeclaw zotero query 'transformer'" );
107
140
  console.error( ' citeclaw zotero cite AB12CD34' );
108
141
  console.error( ' citeclaw batch --op cite --format bibtex --in ./ids.txt --out-jsonl ./result.jsonl' );
142
+ console.error( ' citeclaw translators sync' );
109
143
  console.error( 'options:' );
110
144
  console.error( ' --concurrency <n> batch worker count (default: 4)' );
111
145
  console.error( ' --user-id <id> Zotero user id (or set ZOTERO_USER_ID)' );
@@ -469,13 +503,15 @@ function repoHeadOrMissing( repoPath ) {
469
503
 
470
504
  function translatorsNeedSync() {
471
505
  const stampPath = path.join( stateDir, 'translators-sync.stamp' );
506
+ const activeOfficialDir = resolvedOfficialTranslatorsDir();
507
+ const activeCnDir = resolvedCnTranslatorsDir();
472
508
  const hasMerged = fileExists( mergedTranslatorsDir ) &&
473
509
  fs.readdirSync( mergedTranslatorsDir ).some( ( name ) => name.endsWith( '.js' ) );
474
510
  if ( !hasMerged ) {
475
511
  return true;
476
512
  }
477
513
 
478
- const currentStamp = `zotero=${ repoHeadOrMissing( path.join( zoteroDir, 'modules', 'translators' ) ) };official=${ repoHeadOrMissing( officialTranslatorsDir ) };cn=${ repoHeadOrMissing( cnTranslatorsDir ) }`;
514
+ const currentStamp = `zotero=${ repoHeadOrMissing( path.join( zoteroDir, 'modules', 'translators' ) ) };official=${ repoHeadOrMissing( activeOfficialDir ) };cn=${ repoHeadOrMissing( activeCnDir ) }`;
479
515
  let previousStamp = '';
480
516
  if ( fileExists( stampPath ) ) {
481
517
  previousStamp = fs.readFileSync( stampPath, 'utf8' ).trim();
@@ -485,7 +521,7 @@ function translatorsNeedSync() {
485
521
 
486
522
  function writeTranslatorStamp() {
487
523
  const stampPath = path.join( stateDir, 'translators-sync.stamp' );
488
- const stamp = `zotero=${ repoHeadOrMissing( path.join( zoteroDir, 'modules', 'translators' ) ) };official=${ repoHeadOrMissing( officialTranslatorsDir ) };cn=${ repoHeadOrMissing( cnTranslatorsDir ) }`;
524
+ const stamp = `zotero=${ repoHeadOrMissing( path.join( zoteroDir, 'modules', 'translators' ) ) };official=${ repoHeadOrMissing( resolvedOfficialTranslatorsDir() ) };cn=${ repoHeadOrMissing( resolvedCnTranslatorsDir() ) }`;
489
525
  fs.writeFileSync( stampPath, `${ stamp }\n` );
490
526
  }
491
527
 
@@ -493,6 +529,8 @@ function syncMergedTranslators() {
493
529
  fs.rmSync( mergedTranslatorsDir, { recursive: true, force: true } );
494
530
  fs.mkdirSync( mergedTranslatorsDir, { recursive: true } );
495
531
  const zoteroTranslators = path.join( zoteroDir, 'modules', 'translators' );
532
+ const activeOfficialDir = resolvedOfficialTranslatorsDir();
533
+ const activeCnDir = resolvedCnTranslatorsDir();
496
534
  if ( fileExists( zoteroTranslators ) ) {
497
535
  fs.readdirSync( zoteroTranslators )
498
536
  .filter( ( name ) => name.endsWith( '.js' ) )
@@ -503,22 +541,22 @@ function syncMergedTranslators() {
503
541
  );
504
542
  } );
505
543
  }
506
- if ( fileExists( officialTranslatorsDir ) ) {
507
- fs.readdirSync( officialTranslatorsDir )
544
+ if ( fileExists( activeOfficialDir ) ) {
545
+ fs.readdirSync( activeOfficialDir )
508
546
  .filter( ( name ) => name.endsWith( '.js' ) )
509
547
  .forEach( ( name ) => {
510
548
  fs.copyFileSync(
511
- path.join( officialTranslatorsDir, name ),
549
+ path.join( activeOfficialDir, name ),
512
550
  path.join( mergedTranslatorsDir, name )
513
551
  );
514
552
  } );
515
553
  }
516
- if ( fileExists( cnTranslatorsDir ) ) {
517
- fs.readdirSync( cnTranslatorsDir )
554
+ if ( fileExists( activeCnDir ) ) {
555
+ fs.readdirSync( activeCnDir )
518
556
  .filter( ( name ) => name.endsWith( '.js' ) )
519
557
  .forEach( ( name ) => {
520
558
  fs.copyFileSync(
521
- path.join( cnTranslatorsDir, name ),
559
+ path.join( activeCnDir, name ),
522
560
  path.join( mergedTranslatorsDir, name )
523
561
  );
524
562
  } );
@@ -526,10 +564,51 @@ function syncMergedTranslators() {
526
564
  writeTranslatorStamp();
527
565
  }
528
566
 
567
+ function syncTranslators() {
568
+ ensureDirs();
569
+ const sources = ensureDefaultTranslatorSources();
570
+ syncMergedTranslators();
571
+ const mergedCount = fs.readdirSync( mergedTranslatorsDir )
572
+ .filter( ( name ) => name.endsWith( '.js' ) )
573
+ .length;
574
+ process.stdout.write(
575
+ `translators synced to ${ mergedTranslatorsDir } from ${ sources.officialDir }, ${ sources.cnDir } and ${ path.join( zoteroDir, 'modules', 'translators' ) } (${ mergedCount } files)\n`
576
+ );
577
+ }
578
+
579
+ function ensureDefaultTranslatorSources() {
580
+ const existingOfficial = fileExists( officialTranslatorsDir ) ? officialTranslatorsDir : null;
581
+ const existingCn = fileExists( cnTranslatorsDir ) ? cnTranslatorsDir : null;
582
+ if ( existingOfficial && existingCn ) {
583
+ return {
584
+ officialDir: existingOfficial,
585
+ cnDir: existingCn
586
+ };
587
+ }
588
+ if ( !commandExists( 'git' ) ) {
589
+ throw new Error(
590
+ 'missing translator sources and git is not installed; install git or provide vendored translator directories'
591
+ );
592
+ }
593
+ fs.mkdirSync( localTranslatorSourcesDir, { recursive: true } );
594
+ defaultTranslatorGitSources.forEach( ( source ) => {
595
+ if ( fileExists( source.dir ) ) {
596
+ runCommandOrThrow( 'git', [ '-C', source.dir, 'pull', '--ff-only' ] );
597
+ return;
598
+ }
599
+ runCommandOrThrow( 'git', [ 'clone', '--depth', '1', source.url, source.dir ] );
600
+ } );
601
+ return {
602
+ officialDir: defaultTranslatorGitSources[ 0 ].dir,
603
+ cnDir: defaultTranslatorGitSources[ 1 ].dir
604
+ };
605
+ }
606
+
529
607
  function bootstrapLocalEnvironment() {
530
608
  ensureDirs();
531
609
  const installer = resolveInstallCommand();
532
- if ( !repoReady( zoteroDir ) || !repoReady( cnTranslatorsDir ) || !repoReady( officialTranslatorsDir ) ) {
610
+ const translatorSources = ensureDefaultTranslatorSources();
611
+ if ( !repoReady( zoteroDir ) || !repoReady( translatorSources.cnDir ) || !repoReady( translatorSources.officialDir ) ) {
533
612
  throw new Error(
534
613
  'missing vendored repos under vendor/. pull the complete repository content.'
535
614
  );
@@ -572,6 +651,27 @@ function ensureStyleRuntime() {
572
651
  throw new Error( 'Styles are not synced. Run: citeclaw styles sync' );
573
652
  }
574
653
 
654
+ function ensureDefaultStyleSources() {
655
+ const existing = defaultStyleSources.filter( ( sourceDir ) => fileExists( sourceDir ) );
656
+ if ( existing.length ) {
657
+ return existing;
658
+ }
659
+ if ( !commandExists( 'git' ) ) {
660
+ throw new Error(
661
+ 'no bundled style sources found in this package, and git is not installed; install git or pass --repo <styles-dir>'
662
+ );
663
+ }
664
+ fs.mkdirSync( localStyleSourcesDir, { recursive: true } );
665
+ defaultStyleGitSources.forEach( ( source ) => {
666
+ if ( fileExists( source.dir ) ) {
667
+ runCommandOrThrow( 'git', [ '-C', source.dir, 'pull', '--ff-only' ] );
668
+ return;
669
+ }
670
+ runCommandOrThrow( 'git', [ 'clone', '--depth', '1', source.url, source.dir ] );
671
+ } );
672
+ return defaultStyleGitSources.map( ( source ) => source.dir );
673
+ }
674
+
575
675
  function walkFiles( dirPath ) {
576
676
  const entries = fs.readdirSync( dirPath, { withFileTypes: true } );
577
677
  let result = [];
@@ -1393,16 +1493,11 @@ async function syncStyles( options ) {
1393
1493
  options.repo :
1394
1494
  path.join( rootDir, options.repo || 'vendor/styles' )
1395
1495
  ] :
1396
- defaultStyleSources;
1496
+ ensureDefaultStyleSources();
1397
1497
  const sourceDirs = configuredSourceDirs.filter( ( sourceDir ) => fileExists( sourceDir ) );
1398
1498
  if ( options.repo && !sourceDirs.length ) {
1399
1499
  throw new Error( `styles source not found: ${ configuredSourceDirs.join( ', ' ) }` );
1400
1500
  }
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
1501
 
1407
1502
  let copiedCount = 0;
1408
1503
  sourceDirs.forEach( ( sourceDir ) => {
@@ -4869,6 +4964,22 @@ function main() {
4869
4964
  return;
4870
4965
  }
4871
4966
 
4967
+ if ( action === 'translators' ) {
4968
+ const parsed = parseOptions( process.argv.slice( 3 ) );
4969
+ const subAction = parsed.args[ 0 ];
4970
+ if ( subAction !== 'sync' ) {
4971
+ usage();
4972
+ process.exit( 1 );
4973
+ }
4974
+ try {
4975
+ syncTranslators();
4976
+ process.exit( 0 );
4977
+ } catch ( error ) {
4978
+ handleCommandError( error, parsed, 'translators', 'sync' );
4979
+ }
4980
+ return;
4981
+ }
4982
+
4872
4983
  if ( action === 'api' ) {
4873
4984
  const parsed = parseOptions( process.argv.slice( 3 ) );
4874
4985
  const requestPath = parsed.args.join( ' ' ).trim();