openrxiv-cli 0.0.2

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 (67) hide show
  1. package/dist/api/api-client.d.ts +96 -0
  2. package/dist/api/api-client.d.ts.map +1 -0
  3. package/dist/api/api-client.js +257 -0
  4. package/dist/aws/bucket-explorer.d.ts +26 -0
  5. package/dist/aws/bucket-explorer.d.ts.map +1 -0
  6. package/dist/aws/bucket-explorer.js +220 -0
  7. package/dist/aws/config.d.ts +5 -0
  8. package/dist/aws/config.d.ts.map +1 -0
  9. package/dist/aws/config.js +36 -0
  10. package/dist/aws/downloader.d.ts +13 -0
  11. package/dist/aws/downloader.d.ts.map +1 -0
  12. package/dist/aws/downloader.js +115 -0
  13. package/dist/aws/month-lister.d.ts +18 -0
  14. package/dist/aws/month-lister.d.ts.map +1 -0
  15. package/dist/aws/month-lister.js +90 -0
  16. package/dist/commands/batch-info.d.ts +3 -0
  17. package/dist/commands/batch-info.d.ts.map +1 -0
  18. package/dist/commands/batch-info.js +213 -0
  19. package/dist/commands/batch-process.d.ts +3 -0
  20. package/dist/commands/batch-process.d.ts.map +1 -0
  21. package/dist/commands/batch-process.js +557 -0
  22. package/dist/commands/download.d.ts +3 -0
  23. package/dist/commands/download.d.ts.map +1 -0
  24. package/dist/commands/download.js +76 -0
  25. package/dist/commands/index.d.ts +6 -0
  26. package/dist/commands/index.d.ts.map +1 -0
  27. package/dist/commands/index.js +5 -0
  28. package/dist/commands/list.d.ts +3 -0
  29. package/dist/commands/list.d.ts.map +1 -0
  30. package/dist/commands/list.js +18 -0
  31. package/dist/commands/summary.d.ts +3 -0
  32. package/dist/commands/summary.d.ts.map +1 -0
  33. package/dist/commands/summary.js +249 -0
  34. package/dist/index.d.ts +7 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +35 -0
  37. package/dist/utils/batches.d.ts +9 -0
  38. package/dist/utils/batches.d.ts.map +1 -0
  39. package/dist/utils/batches.js +61 -0
  40. package/dist/utils/batches.test.d.ts +2 -0
  41. package/dist/utils/batches.test.d.ts.map +1 -0
  42. package/dist/utils/batches.test.js +119 -0
  43. package/dist/utils/default-server.d.ts +3 -0
  44. package/dist/utils/default-server.d.ts.map +1 -0
  45. package/dist/utils/default-server.js +20 -0
  46. package/dist/utils/index.d.ts +5 -0
  47. package/dist/utils/index.d.ts.map +1 -0
  48. package/dist/utils/index.js +5 -0
  49. package/dist/utils/meca-processor.d.ts +28 -0
  50. package/dist/utils/meca-processor.d.ts.map +1 -0
  51. package/dist/utils/meca-processor.js +503 -0
  52. package/dist/utils/meca-processor.test.d.ts +2 -0
  53. package/dist/utils/meca-processor.test.d.ts.map +1 -0
  54. package/dist/utils/meca-processor.test.js +123 -0
  55. package/dist/utils/months.d.ts +36 -0
  56. package/dist/utils/months.d.ts.map +1 -0
  57. package/dist/utils/months.js +135 -0
  58. package/dist/utils/months.test.d.ts +2 -0
  59. package/dist/utils/months.test.d.ts.map +1 -0
  60. package/dist/utils/months.test.js +209 -0
  61. package/dist/utils/requester-pays-error.d.ts +6 -0
  62. package/dist/utils/requester-pays-error.d.ts.map +1 -0
  63. package/dist/utils/requester-pays-error.js +20 -0
  64. package/dist/version.d.ts +3 -0
  65. package/dist/version.d.ts.map +1 -0
  66. package/dist/version.js +2 -0
  67. package/package.json +67 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Month utility functions for batch processing
3
+ */
4
+ /**
5
+ * Generate a range of months to process backwards from current month to 2018-12
6
+ */
7
+ export function generateMonthRange() {
8
+ const months = [];
9
+ const now = new Date();
10
+ const currentDate = new Date(now.getFullYear(), now.getMonth(), 1);
11
+ // Go back from current month to December 2018
12
+ while (currentDate.getFullYear() >= 2018) {
13
+ const year = currentDate.getFullYear();
14
+ const month = String(currentDate.getMonth() + 1).padStart(2, '0');
15
+ // Stop at 2018-12 (inclusive)
16
+ if (year === 2018 && month === '12') {
17
+ months.push('2018-12');
18
+ break;
19
+ }
20
+ months.push(`${year}-${month}`);
21
+ // Move to previous month
22
+ currentDate.setMonth(currentDate.getMonth() - 1);
23
+ }
24
+ return months;
25
+ }
26
+ /**
27
+ * Parse month input and return array of months to process
28
+ */
29
+ export function parseMonthInput(monthInput) {
30
+ // Handle comma-separated list
31
+ if (monthInput.includes(',')) {
32
+ const parts = monthInput
33
+ .split(',')
34
+ .map((m) => m.trim())
35
+ .filter((m) => m.length > 0);
36
+ // Process each part (which may contain wildcards)
37
+ const result = [];
38
+ for (const part of parts) {
39
+ if (part.includes('*')) {
40
+ // Expand wildcard pattern
41
+ result.push(...parseWildcardPattern(part));
42
+ }
43
+ else {
44
+ // Single month
45
+ result.push(part);
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+ // Handle wildcard pattern (e.g., "2025-*")
51
+ if (monthInput.includes('*')) {
52
+ return parseWildcardPattern(monthInput);
53
+ }
54
+ // Single month
55
+ return [monthInput];
56
+ }
57
+ /**
58
+ * Parse wildcard pattern like "2025-*" to get all months in that year
59
+ */
60
+ export function parseWildcardPattern(pattern) {
61
+ const months = [];
62
+ // Extract year from pattern
63
+ const yearMatch = pattern.match(/^(\d{4})-\*$/);
64
+ if (!yearMatch) {
65
+ throw new Error(`Invalid wildcard pattern: ${pattern}. Use format like "2025-*"`);
66
+ }
67
+ const year = parseInt(yearMatch[1], 10);
68
+ const now = new Date();
69
+ const currentYear = now.getFullYear();
70
+ const currentMonth = now.getMonth() + 1;
71
+ // Generate all months in the year, but only up to current month if it's current year
72
+ for (let month = 1; month <= 12; month++) {
73
+ if (year === currentYear && month > currentMonth) {
74
+ break; // Don't process future months
75
+ }
76
+ const monthStr = String(month).padStart(2, '0');
77
+ months.push(`${year}-${monthStr}`);
78
+ }
79
+ return months;
80
+ }
81
+ /**
82
+ * Validate month format (YYYY-MM)
83
+ */
84
+ export function validateMonthFormat(month) {
85
+ const monthRegex = /^\d{4}-\d{2}$/;
86
+ if (!monthRegex.test(month)) {
87
+ return false;
88
+ }
89
+ const [year, monthNum] = month.split('-');
90
+ const yearNum = parseInt(year, 10);
91
+ const monthInt = parseInt(monthNum, 10);
92
+ return yearNum <= 2100 && monthInt >= 1 && monthInt <= 12;
93
+ }
94
+ /**
95
+ * Sort months chronologically (oldest first)
96
+ */
97
+ export function sortMonthsChronologically(months) {
98
+ return months.sort((a, b) => {
99
+ const [yearA, monthA] = a.split('-').map(Number);
100
+ const [yearB, monthB] = b.split('-').map(Number);
101
+ if (yearA !== yearB) {
102
+ return yearA - yearB;
103
+ }
104
+ return monthA - monthB;
105
+ });
106
+ }
107
+ /**
108
+ * Get current month in YYYY-MM format
109
+ */
110
+ export function getCurrentMonth() {
111
+ const now = new Date();
112
+ const year = now.getFullYear();
113
+ const month = String(now.getMonth() + 1).padStart(2, '0');
114
+ return `${year}-${month}`;
115
+ }
116
+ /**
117
+ * Get previous month in YYYY-MM format
118
+ */
119
+ export function getPreviousMonth() {
120
+ const now = new Date();
121
+ now.setMonth(now.getMonth() - 1);
122
+ const year = now.getFullYear();
123
+ const month = String(now.getMonth() + 1).padStart(2, '0');
124
+ return `${year}-${month}`;
125
+ }
126
+ /**
127
+ * Check if a month is in the future
128
+ */
129
+ export function isFutureMonth(month) {
130
+ const [year, monthNum] = month.split('-').map(Number);
131
+ const now = new Date();
132
+ const currentYear = now.getFullYear();
133
+ const currentMonth = now.getMonth() + 1;
134
+ return year > currentYear || (year === currentYear && monthNum > currentMonth);
135
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=months.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"months.test.d.ts","sourceRoot":"","sources":["../../src/utils/months.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,209 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { generateMonthRange, parseMonthInput, parseWildcardPattern, validateMonthFormat, sortMonthsChronologically, getCurrentMonth, getPreviousMonth, isFutureMonth, } from './months.js';
3
+ import { getFolderStructure, removeDuplicateFolders, sortFoldersChronologically, } from 'openrxiv-utils';
4
+ describe('Month Utilities', () => {
5
+ beforeEach(() => {
6
+ // Mock the current date to 2025-01-15 for consistent testing
7
+ const mockDate = new Date('2025-01-15T12:00:00Z');
8
+ vi.useFakeTimers();
9
+ vi.setSystemTime(mockDate);
10
+ });
11
+ afterEach(() => {
12
+ vi.useRealTimers();
13
+ });
14
+ describe('generateMonthRange', () => {
15
+ it('should generate months from current month back to 2018-12', () => {
16
+ const months = generateMonthRange();
17
+ // Should start from current month (2025-01)
18
+ expect(months[0]).toBe('2025-01');
19
+ // Should end at 2018-12
20
+ expect(months[months.length - 1]).toBe('2018-12');
21
+ // Should include 2018-12
22
+ expect(months).toContain('2018-12');
23
+ // Should not include 2018-11 or earlier
24
+ expect(months).not.toContain('2018-11');
25
+ expect(months).not.toContain('2017-12');
26
+ // Should have correct number of months
27
+ // From 2025-01 to 2018-12: (2025-2018+1) * 12 = 8 * 12 = 96 months
28
+ // But we stop at 2018-12, so it's actually 74 months
29
+ expect(months).toHaveLength(74);
30
+ });
31
+ it('should generate months in descending order (newest first)', () => {
32
+ const months = generateMonthRange();
33
+ // First few months should be newest
34
+ expect(months[0]).toBe('2025-01');
35
+ expect(months[1]).toBe('2024-12');
36
+ expect(months[2]).toBe('2024-11');
37
+ // Last few months should be oldest
38
+ expect(months[months.length - 3]).toBe('2019-02');
39
+ expect(months[months.length - 2]).toBe('2019-01');
40
+ expect(months[months.length - 1]).toBe('2018-12');
41
+ });
42
+ });
43
+ describe('parseMonthInput', () => {
44
+ it('should handle single month', () => {
45
+ const result = parseMonthInput('2025-01');
46
+ expect(result).toEqual(['2025-01']);
47
+ });
48
+ it('should handle comma-separated months', () => {
49
+ const result = parseMonthInput('2025-01,2024-12,2024-11');
50
+ expect(result).toEqual(['2025-01', '2024-12', '2024-11']);
51
+ });
52
+ it('should handle comma-separated months with spaces', () => {
53
+ const result = parseMonthInput(' 2025-01 , 2024-12 , 2024-11 ');
54
+ expect(result).toEqual(['2025-01', '2024-12', '2024-11']);
55
+ });
56
+ it('should handle wildcard pattern', () => {
57
+ const result = parseMonthInput('2024-*');
58
+ expect(result).toHaveLength(12);
59
+ expect(result).toContain('2024-01');
60
+ expect(result).toContain('2024-12');
61
+ });
62
+ it('should filter out empty strings', () => {
63
+ const result = parseMonthInput('2025-01,,2024-12,');
64
+ expect(result).toEqual(['2025-01', '2024-12']);
65
+ });
66
+ });
67
+ describe('parseWildcardPattern', () => {
68
+ it('should parse valid wildcard pattern', () => {
69
+ const result = parseWildcardPattern('2024-*');
70
+ expect(result).toHaveLength(12);
71
+ expect(result).toContain('2024-01');
72
+ expect(result).toContain('2024-06');
73
+ expect(result).toContain('2024-12');
74
+ });
75
+ it('should handle current year and stop at current month', () => {
76
+ const result = parseWildcardPattern('2025-*');
77
+ // Since we're mocking 2025-01, should only include January
78
+ expect(result).toHaveLength(1);
79
+ expect(result).toEqual(['2025-01']);
80
+ });
81
+ it('should throw error for invalid wildcard pattern', () => {
82
+ expect(() => parseWildcardPattern('2024-')).toThrow('Invalid wildcard pattern');
83
+ expect(() => parseWildcardPattern('2024-*x')).toThrow('Invalid wildcard pattern');
84
+ expect(() => parseWildcardPattern('*-2024')).toThrow('Invalid wildcard pattern');
85
+ });
86
+ it('should handle past years correctly', () => {
87
+ const result = parseWildcardPattern('2020-*');
88
+ expect(result).toHaveLength(12);
89
+ expect(result).toContain('2020-01');
90
+ expect(result).toContain('2020-12');
91
+ });
92
+ });
93
+ describe('validateMonthFormat', () => {
94
+ it('should validate correct month formats', () => {
95
+ expect(validateMonthFormat('2025-01')).toBe(true);
96
+ expect(validateMonthFormat('2024-12')).toBe(true);
97
+ expect(validateMonthFormat('2018-12')).toBe(true);
98
+ expect(validateMonthFormat('2020-06')).toBe(true);
99
+ });
100
+ it('should reject invalid month formats', () => {
101
+ expect(validateMonthFormat('2025-1')).toBe(false); // Missing leading zero
102
+ expect(validateMonthFormat('2025-13')).toBe(false); // Invalid month
103
+ expect(validateMonthFormat('2025-00')).toBe(false); // Invalid month
104
+ expect(validateMonthFormat('2025-1a')).toBe(false); // Non-numeric
105
+ expect(validateMonthFormat('2025')).toBe(false); // Missing month
106
+ expect(validateMonthFormat('2025-01-01')).toBe(false); // Too many parts
107
+ });
108
+ it('should reject months outside valid year range', () => {
109
+ expect(validateMonthFormat('2101-01')).toBe(false); // After 2100
110
+ });
111
+ it('should accept boundary years', () => {
112
+ expect(validateMonthFormat('2018-01')).toBe(true);
113
+ expect(validateMonthFormat('2018-12')).toBe(true);
114
+ expect(validateMonthFormat('2100-01')).toBe(true);
115
+ expect(validateMonthFormat('2100-12')).toBe(true);
116
+ });
117
+ });
118
+ describe('sortMonthsChronologically', () => {
119
+ it('should sort months in chronological order (oldest first)', () => {
120
+ const months = ['2025-01', '2024-12', '2024-01', '2025-02'];
121
+ const sorted = sortMonthsChronologically(months);
122
+ expect(sorted).toEqual(['2024-01', '2024-12', '2025-01', '2025-02']);
123
+ });
124
+ it('should handle months within same year', () => {
125
+ const months = ['2024-12', '2024-01', '2024-06'];
126
+ const sorted = sortMonthsChronologically(months);
127
+ expect(sorted).toEqual(['2024-01', '2024-06', '2024-12']);
128
+ });
129
+ it('should handle single month', () => {
130
+ const months = ['2024-06'];
131
+ const sorted = sortMonthsChronologically(months);
132
+ expect(sorted).toEqual(['2024-06']);
133
+ });
134
+ it('should handle empty array', () => {
135
+ const months = [];
136
+ const sorted = sortMonthsChronologically(months);
137
+ expect(sorted).toEqual([]);
138
+ });
139
+ });
140
+ describe('getCurrentMonth', () => {
141
+ it('should return current month in YYYY-MM format', () => {
142
+ const currentMonth = getCurrentMonth();
143
+ expect(currentMonth).toBe('2025-01');
144
+ });
145
+ });
146
+ describe('getPreviousMonth', () => {
147
+ it('should return previous month in YYYY-MM format', () => {
148
+ const previousMonth = getPreviousMonth();
149
+ expect(previousMonth).toBe('2024-12');
150
+ });
151
+ it('should handle year boundary correctly', () => {
152
+ // Mock to January 2024 to test year boundary
153
+ vi.setSystemTime(new Date('2024-01-15T12:00:00Z'));
154
+ const previousMonth = getPreviousMonth();
155
+ expect(previousMonth).toBe('2023-12');
156
+ });
157
+ });
158
+ describe('isFutureMonth', () => {
159
+ it('should identify future months correctly', () => {
160
+ expect(isFutureMonth('2025-02')).toBe(true);
161
+ expect(isFutureMonth('2025-12')).toBe(true);
162
+ expect(isFutureMonth('2026-01')).toBe(true);
163
+ });
164
+ it('should identify current and past months correctly', () => {
165
+ expect(isFutureMonth('2025-01')).toBe(false); // Current month
166
+ expect(isFutureMonth('2024-12')).toBe(false); // Past month
167
+ expect(isFutureMonth('2024-01')).toBe(false); // Past month
168
+ expect(isFutureMonth('2018-12')).toBe(false); // Past month
169
+ });
170
+ it('should handle year boundaries correctly', () => {
171
+ expect(isFutureMonth('2024-12')).toBe(false); // Past year
172
+ expect(isFutureMonth('2026-01')).toBe(true); // Future year
173
+ });
174
+ });
175
+ describe('Integration tests', () => {
176
+ it('should handle complete workflow: parse, validate, sort, deduplicate', () => {
177
+ const input = '2025-01,2024-12,2025-01,2024-11,2024-12';
178
+ // Parse input
179
+ const parsed = parseMonthInput(input);
180
+ expect(parsed).toEqual(['2025-01', '2024-12', '2025-01', '2024-11', '2024-12']);
181
+ // Validate all months
182
+ const valid = parsed.filter(validateMonthFormat);
183
+ expect(valid).toEqual(['2025-01', '2024-12', '2025-01', '2024-11', '2024-12']);
184
+ // Remove duplicates
185
+ const folders = valid.map((month) => getFolderStructure({ month }));
186
+ const unique = removeDuplicateFolders(folders);
187
+ expect(unique.length).toEqual(3);
188
+ // Sort chronologically
189
+ const sorted = sortFoldersChronologically(unique);
190
+ expect(sorted.length).toEqual(3);
191
+ expect(sorted[0].batch).toEqual('November_2024');
192
+ expect(sorted[1].batch).toEqual('December_2024');
193
+ expect(sorted[2].batch).toEqual('January_2025');
194
+ });
195
+ it('should handle wildcard with validation and sorting', () => {
196
+ const input = '2024-*';
197
+ // Parse wildcard
198
+ const parsed = parseMonthInput(input);
199
+ expect(parsed).toHaveLength(12);
200
+ // Validate all months
201
+ const valid = parsed.filter(validateMonthFormat);
202
+ expect(valid).toHaveLength(12);
203
+ // Sort chronologically
204
+ const sorted = sortMonthsChronologically(valid);
205
+ expect(sorted[0]).toBe('2024-01');
206
+ expect(sorted[11]).toBe('2024-12');
207
+ });
208
+ });
209
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Utility function to display the requester-pays error message
3
+ * Used when S3 operations fail due to requester-pays bucket requirements
4
+ */
5
+ export declare function displayRequesterPaysError(): void;
6
+ //# sourceMappingURL=requester-pays-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requester-pays-error.d.ts","sourceRoot":"","sources":["../../src/utils/requester-pays-error.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAehD"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Utility function to display the requester-pays error message
3
+ * Used when S3 operations fail due to requester-pays bucket requirements
4
+ */
5
+ export function displayRequesterPaysError() {
6
+ console.error(`
7
+ ❌ Operation failed: S3 bucket requires requester-pays
8
+ 💡 This bucket has requester-pays enabled, which means:
9
+ • You need to pay for data transfer costs
10
+ • Your AWS credentials must be configured
11
+ • The bucket policy must allow your account
12
+
13
+ 🔧 To fix this:
14
+ 1. Ensure your AWS credentials are configured
15
+ 2. Verify you have permission to access the bucket
16
+ 3. Add the --requester-pays flag to your command
17
+
18
+ 📚 For more help, see: https://docs.aws.amazon.com/AmazonS3/latest/userguide/RequesterPaysBuckets.html
19
+ `);
20
+ }
@@ -0,0 +1,3 @@
1
+ declare const version = "0.0.2";
2
+ export default version;
3
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,OAAO,UAAU,CAAC;AACxB,eAAe,OAAO,CAAC"}
@@ -0,0 +1,2 @@
1
+ const version = '0.0.2';
2
+ export default version;
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "openrxiv-cli",
3
+ "version": "0.0.2",
4
+ "description": "CLI tool to download openRxiv MECA files from AWS S3 for text and data mining",
5
+ "main": "dist/index.js",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "type": "module",
10
+ "scripts": {
11
+ "clean": "rimraf dist",
12
+ "lint": "eslint \"src/**/!(*.spec).ts\" -c ../../.eslintrc.cjs",
13
+ "lint:format": "npx prettier --check \"src/**/*.ts\"",
14
+ "test": "vitest run",
15
+ "copy:version": "echo \"const version = '\"$npm_package_version\"';\nexport default version;\" > src/version.ts",
16
+ "test:watch": "vitest watch",
17
+ "build:esm": "tsc",
18
+ "build": "npm-run-all -l clean copy:version -p build:esm"
19
+ },
20
+ "keywords": [
21
+ "biorxiv",
22
+ "cli",
23
+ "aws",
24
+ "s3",
25
+ "download",
26
+ "meca",
27
+ "research",
28
+ "text-mining",
29
+ "data-mining"
30
+ ],
31
+ "author": "Curvenote",
32
+ "license": "MIT",
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "dependencies": {
37
+ "@aws-sdk/client-s3": "^3.0.0",
38
+ "@aws-sdk/s3-request-presigner": "^3.0.0",
39
+ "axios": "^1.6.0",
40
+ "openrxiv-utils": "^0.0.2",
41
+ "boxen": "^8.0.1",
42
+ "character-entities": "^2.0.2",
43
+ "chalk": "^5.0.0",
44
+ "cli-progress": "^3.12.0",
45
+ "commander": "^14.0.0",
46
+ "conf": "^10.0.0",
47
+ "inquirer": "^9.0.0",
48
+ "jszip": "^3.10.1",
49
+ "ora": "^7.0.0",
50
+ "adm-zip": "^0.5.10",
51
+ "unified": "^11.0.0",
52
+ "xast-util-from-xml": "^4.0.0",
53
+ "p-limit": "^7.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/cli-progress": "^3.11.0",
57
+ "@types/inquirer": "^9.0.0"
58
+ },
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "https://github.com/continuous-foundation/openrxiv.git"
62
+ },
63
+ "bugs": {
64
+ "url": "https://github.com/continuous-foundation/openrxiv/issues"
65
+ },
66
+ "homepage": "https://github.com/continuous-foundation/openrxiv#readme"
67
+ }