citeclaw 2.0.8 → 2.0.10

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/README.md CHANGED
@@ -37,18 +37,7 @@ npx citeclaw citoid bibtex "https://aclanthology.org/2023.emnlp-main.398/"
37
37
  npx citeclaw cite mediawiki "https://arxiv.org/abs/1706.03762"
38
38
  ```
39
39
 
40
- Fresh npm installs can run normal citation commands directly. `CiteClaw` will bootstrap Zotero and build a local translator runtime automatically from Zotero's bundled translator set.
41
-
42
- Runtime state is persisted in a user-level cache directory by default:
43
-
44
- - Linux: `~/.cache/citeclaw`
45
- - macOS: `~/Library/Caches/citeclaw`
46
- - Windows: `%LOCALAPPDATA%\\citeclaw`
47
-
48
- Override it with `CITECLAW_HOME=/path/to/runtime`.
49
-
50
- By default, the Zotero translation server checkout is also stored under this runtime root, at `runtime/zotero`.
51
- Override it with `ZOTERO_DIR=/path/to/zotero`.
40
+ Fresh npm installs can run normal citation commands directly. `CiteClaw` will bootstrap Zotero and build a local translator runtime automatically from the bundled Zotero translator set.
52
41
 
53
42
  ## Local Service
54
43
 
@@ -176,6 +165,23 @@ Current package metadata and entrypoints live in:
176
165
  - [package.json](/mnt/e/botcite/package.json)
177
166
  - [scripts/citeclaw.js](/mnt/e/botcite/scripts/citeclaw.js)
178
167
 
168
+ ## npm Publishing
169
+
170
+ This repo includes a GitHub Actions workflow for npm publishing:
171
+
172
+ - workflow file: `.github/workflows/npm-publish.yml`
173
+ - trigger: push a tag like `v2.0.8` that matches `package.json`'s `version`
174
+ - required secret: add `NPM_TOKEN` in GitHub repository settings with publish permission for the `citeclaw` package
175
+
176
+ Typical release flow:
177
+
178
+ ```bash
179
+ npm version patch
180
+ git push origin master --follow-tags
181
+ ```
182
+
183
+ The workflow installs dependencies with `npm ci`, checks that the tag matches `package.json`, validates the tarball with `npm pack --dry-run`, and then runs `npm publish --access public --provenance`.
184
+
179
185
  ## Notes
180
186
 
181
187
  - The npm package name and primary CLI are `citeclaw`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "citeclaw",
3
- "version": "2.0.8",
3
+ "version": "2.0.10",
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",
@@ -19,31 +19,13 @@ const yaml = require( 'js-yaml' );
19
19
  const citoidApp = require( '../app.js' );
20
20
 
21
21
  const rootDir = path.resolve( __dirname, '..' );
22
+ const zoteroDir = process.env.ZOTERO_DIR || path.join( rootDir, 'vendor', 'zotero' );
22
23
  const cnTranslatorsDir = process.env.CN_TRANSLATORS_DIR || path.join( rootDir, 'vendor', 'translators_CN' );
23
24
  const officialTranslatorsDir = process.env.OFFICIAL_TRANSLATORS_DIR ||
24
25
  path.join( rootDir, 'vendor', 'translators-official' );
25
26
  const vendoredStylesDir = path.join( rootDir, 'vendor', 'styles' );
26
27
  const vendoredOfficialStylesDir = path.join( rootDir, 'vendor', 'styles-official' );
27
-
28
- function defaultRuntimeRoot() {
29
- if ( process.env.CITECLAW_HOME ) {
30
- return path.resolve( process.env.CITECLAW_HOME );
31
- }
32
- if ( process.env.XDG_CACHE_HOME ) {
33
- return path.join( process.env.XDG_CACHE_HOME, 'citeclaw' );
34
- }
35
- if ( process.platform === 'win32' ) {
36
- const base = process.env.LOCALAPPDATA || process.env.APPDATA || path.join( os.homedir(), 'AppData', 'Local' );
37
- return path.join( base, 'citeclaw' );
38
- }
39
- if ( process.platform === 'darwin' ) {
40
- return path.join( os.homedir(), 'Library', 'Caches', 'citeclaw' );
41
- }
42
- return path.join( os.homedir(), '.cache', 'citeclaw' );
43
- }
44
-
45
- const localDir = defaultRuntimeRoot();
46
- const zoteroDir = process.env.ZOTERO_DIR || path.join( localDir, 'runtime', 'zotero' );
28
+ const localDir = path.join( rootDir, '.local' );
47
29
  const logDir = path.join( localDir, 'logs' );
48
30
  const stateDir = path.join( localDir, 'state' );
49
31
  const localTranslatorSourcesDir = path.join( localDir, 'translator-sources' );
@@ -183,7 +165,7 @@ function usageZotero( subAction = '' ) {
183
165
  console.error( ' citeclaw zotero login --library-type groups --library-id <group-id> --api-key <key>' );
184
166
  console.error( 'notes:' );
185
167
  console.error( ' - personal library defaults to library-type users' );
186
- console.error( ` - credentials are saved to ${ zoteroAuthPath }` );
168
+ console.error( ' - credentials are saved to .local/state/zotero-auth.json' );
187
169
  console.error( ' - API key can be created at https://www.zotero.org/settings/keys' );
188
170
  return;
189
171
  }
@@ -191,7 +173,7 @@ function usageZotero( subAction = '' ) {
191
173
  console.error( 'usage:' );
192
174
  console.error( ' citeclaw zotero logout' );
193
175
  console.error( 'notes:' );
194
- console.error( ` - removes local credentials from ${ zoteroAuthPath }` );
176
+ console.error( ' - removes local credentials from .local/state/zotero-auth.json' );
195
177
  return;
196
178
  }
197
179
  if ( action === 'whoami' ) {
@@ -361,6 +343,24 @@ function fileExists( filePath ) {
361
343
  }
362
344
  }
363
345
 
346
+ function normalizeCommandForSpawn( command, platform ) {
347
+ const activePlatform = platform || process.platform;
348
+ if ( activePlatform !== 'win32' ) {
349
+ return command;
350
+ }
351
+ if ( /[\\/]/.test( command ) || /\.[^\\/]+$/.test( command ) ) {
352
+ return command;
353
+ }
354
+ const windowsCommands = {
355
+ npm: 'npm.cmd',
356
+ npx: 'npx.cmd',
357
+ git: 'git.exe',
358
+ curl: 'curl.exe',
359
+ bun: 'bun.exe'
360
+ };
361
+ return windowsCommands[ command ] || command;
362
+ }
363
+
364
364
  function commandExists( command ) {
365
365
  const args = process.platform === 'win32' ? [ '/c', 'where', command ] : [ '-lc', `command -v ${ command }` ];
366
366
  const found = spawnSync( process.platform === 'win32' ? 'cmd' : 'bash', args, {
@@ -457,7 +457,7 @@ function jsonOut( value ) {
457
457
  }
458
458
 
459
459
  function runCommandText( command, args ) {
460
- const result = spawnSync( command, args, {
460
+ const result = spawnSync( normalizeCommandForSpawn( command ), args, {
461
461
  stdio: 'pipe',
462
462
  encoding: 'utf8'
463
463
  } );
@@ -472,7 +472,7 @@ function runCommandText( command, args ) {
472
472
  }
473
473
 
474
474
  function runCommandOrThrow( command, args, cwd ) {
475
- const result = spawnSync( command, args, {
475
+ const result = spawnSync( normalizeCommandForSpawn( command ), args, {
476
476
  cwd: cwd || rootDir,
477
477
  stdio: 'pipe',
478
478
  encoding: 'utf8'
@@ -487,6 +487,46 @@ function runCommandOrThrow( command, args, cwd ) {
487
487
  return result;
488
488
  }
489
489
 
490
+ function styleLocaleUrls( locale ) {
491
+ const fileName = `locales-${ locale }.xml`;
492
+ return [
493
+ `https://raw.githubusercontent.com/citation-style-language/locales/master/${ fileName }`,
494
+ `https://github.com/citation-style-language/locales/raw/master/${ fileName }`,
495
+ `https://cdn.jsdelivr.net/gh/citation-style-language/locales@master/${ fileName }`
496
+ ];
497
+ }
498
+
499
+ function downloadFileWithCurlFallbacks( urls, dest, label, runner ) {
500
+ const runCommand = runner || runCommandOrThrow;
501
+ const candidates = Array.isArray( urls ) ? urls : [ urls ];
502
+ const targetLabel = label || path.basename( dest );
503
+ const tempDest = `${ dest }.tmp`;
504
+ const failures = [];
505
+ fs.mkdirSync( path.dirname( dest ), { recursive: true } );
506
+
507
+ for ( const url of candidates ) {
508
+ try {
509
+ fs.rmSync( tempDest, { force: true } );
510
+ runCommand( 'curl', [
511
+ '-fsSL',
512
+ '--retry', '3',
513
+ '--retry-delay', '1',
514
+ '--retry-all-errors',
515
+ url,
516
+ '-o', tempDest
517
+ ] );
518
+ fs.rmSync( dest, { force: true } );
519
+ fs.renameSync( tempDest, dest );
520
+ return;
521
+ } catch ( error ) {
522
+ failures.push( `${ url }: ${ error.message }` );
523
+ }
524
+ }
525
+
526
+ fs.rmSync( tempDest, { force: true } );
527
+ throw new Error( `failed to download ${ targetLabel }: ${ failures.join( ' | ' ) }` );
528
+ }
529
+
490
530
  function resolveInstallCommand() {
491
531
  if ( commandExists( 'npm' ) ) {
492
532
  return { command: 'npm', args: [ 'install' ] };
@@ -657,7 +697,7 @@ function bootstrapLocalEnvironment() {
657
697
  runCommandOrThrow( installer.command, installer.args, rootDir );
658
698
  }
659
699
  if ( !fileExists( path.join( zoteroDir, 'modules', 'translators' ) ) ) {
660
- throw new Error( `missing zotero translator contents under ${ path.join( zoteroDir, 'modules', 'translators' ) }` );
700
+ throw new Error( 'missing vendored zotero contents under vendor/zotero/modules/translators' );
661
701
  }
662
702
  if ( !fileExists( path.join( zoteroDir, 'node_modules' ) ) ) {
663
703
  runCommandOrThrow( installer.command, installer.args, zoteroDir );
@@ -1522,19 +1562,24 @@ async function runCitationFromPdf( pdfPath, options ) {
1522
1562
  } );
1523
1563
  }
1524
1564
 
1525
- function syncStyles( options ) {
1565
+ function syncStyles( options, dependencies ) {
1566
+ const styleOptions = options || {};
1567
+ const hooks = dependencies || {};
1568
+ const downloadFile = hooks.downloadFile || downloadFileWithCurlFallbacks;
1569
+ const downloadUsesCurl = downloadFile === downloadFileWithCurlFallbacks;
1570
+
1526
1571
  ensureDirs();
1527
1572
  fs.mkdirSync( cslDir, { recursive: true } );
1528
1573
  fs.mkdirSync( localeDir, { recursive: true } );
1529
- const configuredSourceDirs = options.repo ?
1574
+ const configuredSourceDirs = styleOptions.repo ?
1530
1575
  [
1531
- path.isAbsolute( options.repo || '' ) ?
1532
- options.repo :
1533
- path.join( rootDir, options.repo || 'vendor/styles' )
1576
+ path.isAbsolute( styleOptions.repo || '' ) ?
1577
+ styleOptions.repo :
1578
+ path.join( rootDir, styleOptions.repo || 'vendor/styles' )
1534
1579
  ] :
1535
1580
  ensureDefaultStyleSources();
1536
1581
  const sourceDirs = configuredSourceDirs.filter( ( sourceDir ) => fileExists( sourceDir ) );
1537
- if ( options.repo && !sourceDirs.length ) {
1582
+ if ( styleOptions.repo && !sourceDirs.length ) {
1538
1583
  throw new Error( `styles source not found: ${ configuredSourceDirs.join( ', ' ) }` );
1539
1584
  }
1540
1585
 
@@ -1550,13 +1595,12 @@ function syncStyles( options ) {
1550
1595
  } );
1551
1596
 
1552
1597
  const localeTargets = [ 'en-US', 'zh-CN' ];
1553
- if ( !commandExists( 'curl' ) ) {
1598
+ if ( downloadUsesCurl && !commandExists( 'curl' ) ) {
1554
1599
  throw new Error( 'curl is required for styles sync' );
1555
1600
  }
1556
1601
  for ( const locale of localeTargets ) {
1557
- const localeUrl = `https://raw.githubusercontent.com/citation-style-language/locales/master/locales-${ locale }.xml`;
1558
1602
  const dest = path.join( localeDir, `locales-${ locale }.xml` );
1559
- runCommandOrThrow( 'curl', [ '-fsSL', localeUrl, '-o', dest ] );
1603
+ downloadFile( styleLocaleUrls( locale ), dest, `locale ${ locale }` );
1560
1604
  }
1561
1605
 
1562
1606
  process.stdout.write( `styles synced to ${ cslDir } from ${ sourceDirs.join( ', ' ) } (copied ${ copiedCount } files)\n` );
@@ -4997,9 +5041,11 @@ function main() {
4997
5041
  usage();
4998
5042
  process.exit( 1 );
4999
5043
  }
5000
- syncStyles( parsed ).catch( ( error ) => {
5044
+ try {
5045
+ syncStyles( parsed );
5046
+ } catch ( error ) {
5001
5047
  handleCommandError( error, parsed, 'styles', 'sync' );
5002
- } );
5048
+ }
5003
5049
  return;
5004
5050
  }
5005
5051
 
@@ -5158,12 +5204,15 @@ function main() {
5158
5204
  }
5159
5205
 
5160
5206
  module.exports = {
5207
+ downloadFileWithCurlFallbacks,
5161
5208
  detectPdfIdentifierCandidates,
5162
5209
  extractBestDoiCandidate,
5163
5210
  extractPdfCandidates,
5164
5211
  main,
5212
+ normalizeCommandForSpawn,
5165
5213
  normalizeArxivId,
5166
5214
  normalizeDoi,
5215
+ syncStyles,
5167
5216
  shouldAttemptPdfOcr
5168
5217
  };
5169
5218