@ncukondo/search-hub 0.7.0 → 0.9.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.
- package/README.md +24 -0
- package/dist/cli/commands/config.d.ts.map +1 -1
- package/dist/cli/commands/config.js +1 -0
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/fulltext/attach.d.ts +12 -0
- package/dist/cli/commands/fulltext/attach.d.ts.map +1 -0
- package/dist/cli/commands/fulltext/attach.js +67 -0
- package/dist/cli/commands/fulltext/attach.js.map +1 -0
- package/dist/cli/commands/fulltext/check.d.ts +30 -0
- package/dist/cli/commands/fulltext/check.d.ts.map +1 -0
- package/dist/cli/commands/fulltext/check.js +113 -0
- package/dist/cli/commands/fulltext/check.js.map +1 -0
- package/dist/cli/commands/fulltext/convert.d.ts +22 -0
- package/dist/cli/commands/fulltext/convert.d.ts.map +1 -0
- package/dist/cli/commands/fulltext/convert.js +84 -0
- package/dist/cli/commands/fulltext/convert.js.map +1 -0
- package/dist/cli/commands/fulltext/fetch.d.ts +35 -0
- package/dist/cli/commands/fulltext/fetch.d.ts.map +1 -0
- package/dist/cli/commands/fulltext/fetch.js +130 -0
- package/dist/cli/commands/fulltext/fetch.js.map +1 -0
- package/dist/cli/commands/fulltext/format.d.ts +7 -0
- package/dist/cli/commands/fulltext/format.d.ts.map +1 -0
- package/dist/cli/commands/fulltext/format.js +72 -0
- package/dist/cli/commands/fulltext/format.js.map +1 -0
- package/dist/cli/commands/fulltext/index.d.ts +4 -0
- package/dist/cli/commands/fulltext/index.d.ts.map +1 -0
- package/dist/cli/commands/fulltext/index.js +326 -0
- package/dist/cli/commands/fulltext/index.js.map +1 -0
- package/dist/cli/commands/fulltext/init.d.ts +23 -0
- package/dist/cli/commands/fulltext/init.d.ts.map +1 -0
- package/dist/cli/commands/fulltext/init.js +88 -0
- package/dist/cli/commands/fulltext/init.js.map +1 -0
- package/dist/cli/commands/fulltext/pending.d.ts +30 -0
- package/dist/cli/commands/fulltext/pending.d.ts.map +1 -0
- package/dist/cli/commands/fulltext/pending.js +88 -0
- package/dist/cli/commands/fulltext/pending.js.map +1 -0
- package/dist/cli/commands/fulltext/status.d.ts +23 -0
- package/dist/cli/commands/fulltext/status.d.ts.map +1 -0
- package/dist/cli/commands/fulltext/status.js +68 -0
- package/dist/cli/commands/fulltext/status.js.map +1 -0
- package/dist/cli/commands/fulltext/sync.d.ts +23 -0
- package/dist/cli/commands/fulltext/sync.d.ts.map +1 -0
- package/dist/cli/commands/fulltext/sync.js +115 -0
- package/dist/cli/commands/fulltext/sync.js.map +1 -0
- package/dist/cli/commands/register.d.ts +1 -5
- package/dist/cli/commands/register.d.ts.map +1 -1
- package/dist/cli/commands/register.js +0 -8
- package/dist/cli/commands/register.js.map +1 -1
- package/dist/cli/commands/review/extract.d.ts +7 -2
- package/dist/cli/commands/review/extract.d.ts.map +1 -1
- package/dist/cli/commands/review/extract.js +40 -9
- package/dist/cli/commands/review/extract.js.map +1 -1
- package/dist/cli/commands/review/finalize.d.ts +22 -0
- package/dist/cli/commands/review/finalize.d.ts.map +1 -0
- package/dist/cli/commands/review/finalize.js +88 -0
- package/dist/cli/commands/review/finalize.js.map +1 -0
- package/dist/cli/commands/review/init.d.ts.map +1 -1
- package/dist/cli/commands/review/init.js +4 -5
- package/dist/cli/commands/review/init.js.map +1 -1
- package/dist/cli/commands/review/list.d.ts +1 -18
- package/dist/cli/commands/review/list.d.ts.map +1 -1
- package/dist/cli/commands/review/list.js +3 -19
- package/dist/cli/commands/review/list.js.map +1 -1
- package/dist/cli/commands/review/mark.d.ts +3 -5
- package/dist/cli/commands/review/mark.d.ts.map +1 -1
- package/dist/cli/commands/review/mark.js +11 -27
- package/dist/cli/commands/review/mark.js.map +1 -1
- package/dist/cli/commands/review/merge.d.ts +8 -5
- package/dist/cli/commands/review/merge.d.ts.map +1 -1
- package/dist/cli/commands/review/merge.js +40 -24
- package/dist/cli/commands/review/merge.js.map +1 -1
- package/dist/cli/commands/review/next-steps.d.ts +42 -0
- package/dist/cli/commands/review/next-steps.d.ts.map +1 -0
- package/dist/cli/commands/review/next-steps.js +66 -0
- package/dist/cli/commands/review/next-steps.js.map +1 -0
- package/dist/cli/commands/review/status.d.ts +7 -4
- package/dist/cli/commands/review/status.d.ts.map +1 -1
- package/dist/cli/commands/review/status.js +34 -23
- package/dist/cli/commands/review/status.js.map +1 -1
- package/dist/cli/commands/review/types.d.ts +26 -12
- package/dist/cli/commands/review/types.d.ts.map +1 -1
- package/dist/cli/commands/review/types.js +26 -8
- package/dist/cli/commands/review/types.js.map +1 -1
- package/dist/cli/commands/search-executor.d.ts +1 -0
- package/dist/cli/commands/search-executor.d.ts.map +1 -1
- package/dist/cli/commands/search-executor.js +7 -1
- package/dist/cli/commands/search-executor.js.map +1 -1
- package/dist/cli/commands/search.d.ts +0 -15
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/search.js +0 -17
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +146 -56
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/suggestions/conditions.d.ts +10 -0
- package/dist/cli/suggestions/conditions.d.ts.map +1 -0
- package/dist/cli/suggestions/index.d.ts +17 -0
- package/dist/cli/suggestions/index.d.ts.map +1 -0
- package/dist/cli/suggestions/index.js +25 -0
- package/dist/cli/suggestions/index.js.map +1 -0
- package/dist/cli/suggestions/rules.d.ts +7 -0
- package/dist/cli/suggestions/rules.d.ts.map +1 -0
- package/dist/cli/suggestions/rules.js +353 -0
- package/dist/cli/suggestions/rules.js.map +1 -0
- package/dist/cli/suggestions/types.d.ts +58 -0
- package/dist/cli/suggestions/types.d.ts.map +1 -0
- package/dist/cli/utils/sessions-dir.js +1 -1
- package/dist/cli/utils/sessions-dir.js.map +1 -1
- package/dist/config/loader.d.ts +11 -5
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +6 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +14 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +34 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/fulltext/attach-shared.d.ts +43 -0
- package/dist/fulltext/attach-shared.d.ts.map +1 -0
- package/dist/fulltext/attach-shared.js +118 -0
- package/dist/fulltext/attach-shared.js.map +1 -0
- package/dist/fulltext/citation-key.d.ts +15 -0
- package/dist/fulltext/citation-key.d.ts.map +1 -0
- package/dist/fulltext/citation-key.js +44 -0
- package/dist/fulltext/citation-key.js.map +1 -0
- package/dist/fulltext/convert/index.d.ts +20 -0
- package/dist/fulltext/convert/index.d.ts.map +1 -0
- package/dist/fulltext/convert/index.js +44 -0
- package/dist/fulltext/convert/index.js.map +1 -0
- package/dist/fulltext/convert/jats-parser.d.ts +23 -0
- package/dist/fulltext/convert/jats-parser.d.ts.map +1 -0
- package/dist/fulltext/convert/jats-parser.js +484 -0
- package/dist/fulltext/convert/jats-parser.js.map +1 -0
- package/dist/fulltext/convert/markdown-writer.d.ts +6 -0
- package/dist/fulltext/convert/markdown-writer.d.ts.map +1 -0
- package/dist/fulltext/convert/markdown-writer.js +119 -0
- package/dist/fulltext/convert/markdown-writer.js.map +1 -0
- package/dist/fulltext/convert/types.d.ts +79 -0
- package/dist/fulltext/convert/types.d.ts.map +1 -0
- package/dist/fulltext/discovery/arxiv.d.ts +11 -0
- package/dist/fulltext/discovery/arxiv.d.ts.map +1 -0
- package/dist/fulltext/discovery/arxiv.js +19 -0
- package/dist/fulltext/discovery/arxiv.js.map +1 -0
- package/dist/fulltext/discovery/core.d.ts +11 -0
- package/dist/fulltext/discovery/core.d.ts.map +1 -0
- package/dist/fulltext/discovery/core.js +51 -0
- package/dist/fulltext/discovery/core.js.map +1 -0
- package/dist/fulltext/discovery/index.d.ts +28 -0
- package/dist/fulltext/discovery/index.d.ts.map +1 -0
- package/dist/fulltext/discovery/index.js +75 -0
- package/dist/fulltext/discovery/index.js.map +1 -0
- package/dist/fulltext/discovery/pmc.d.ts +19 -0
- package/dist/fulltext/discovery/pmc.d.ts.map +1 -0
- package/dist/fulltext/discovery/pmc.js +57 -0
- package/dist/fulltext/discovery/pmc.js.map +1 -0
- package/dist/fulltext/discovery/unpaywall.d.ts +11 -0
- package/dist/fulltext/discovery/unpaywall.d.ts.map +1 -0
- package/dist/fulltext/discovery/unpaywall.js +57 -0
- package/dist/fulltext/discovery/unpaywall.js.map +1 -0
- package/dist/fulltext/download/downloader.d.ts +21 -0
- package/dist/fulltext/download/downloader.d.ts.map +1 -0
- package/dist/fulltext/download/downloader.js +59 -0
- package/dist/fulltext/download/downloader.js.map +1 -0
- package/dist/fulltext/download/orchestrator.d.ts +33 -0
- package/dist/fulltext/download/orchestrator.d.ts.map +1 -0
- package/dist/fulltext/download/orchestrator.js +125 -0
- package/dist/fulltext/download/orchestrator.js.map +1 -0
- package/dist/fulltext/download/pmc-xml.d.ts +13 -0
- package/dist/fulltext/download/pmc-xml.d.ts.map +1 -0
- package/dist/fulltext/download/pmc-xml.js +48 -0
- package/dist/fulltext/download/pmc-xml.js.map +1 -0
- package/dist/fulltext/meta.d.ts +25 -0
- package/dist/fulltext/meta.d.ts.map +1 -0
- package/dist/fulltext/meta.js +45 -0
- package/dist/fulltext/meta.js.map +1 -0
- package/dist/fulltext/paths.d.ts +12 -0
- package/dist/fulltext/paths.d.ts.map +1 -0
- package/dist/fulltext/paths.js +20 -0
- package/dist/fulltext/paths.js.map +1 -0
- package/dist/fulltext/readme.d.ts +4 -0
- package/dist/fulltext/readme.d.ts.map +1 -0
- package/dist/fulltext/readme.js +58 -0
- package/dist/fulltext/readme.js.map +1 -0
- package/dist/fulltext/types.d.ts +90 -0
- package/dist/fulltext/types.d.ts.map +1 -0
- package/dist/integration/fulltext-attach.d.ts +28 -0
- package/dist/integration/fulltext-attach.d.ts.map +1 -0
- package/dist/integration/fulltext-attach.js +42 -0
- package/dist/integration/fulltext-attach.js.map +1 -0
- package/dist/integration/ref-cli.d.ts +7 -0
- package/dist/integration/ref-cli.d.ts.map +1 -1
- package/dist/integration/ref-cli.js +49 -1
- package/dist/integration/ref-cli.js.map +1 -1
- package/dist/integration/register.d.ts +3 -0
- package/dist/integration/register.d.ts.map +1 -1
- package/dist/integration/register.js +23 -1
- package/dist/integration/register.js.map +1 -1
- package/dist/integration/types.d.ts +52 -0
- package/dist/integration/types.d.ts.map +1 -1
- package/dist/integration/types.js +27 -1
- package/dist/integration/types.js.map +1 -1
- package/dist/node_modules/any-ascii/any-ascii.js +26 -0
- package/dist/node_modules/any-ascii/any-ascii.js.map +1 -0
- package/dist/node_modules/any-ascii/block.js +1289 -0
- package/dist/node_modules/any-ascii/block.js.map +1 -0
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +8 -7
- package/dist/session/manager.js.map +1 -1
- package/dist/session/types.d.ts +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A CLI tool for systematic literature searching across multiple academic database
|
|
|
11
11
|
- **Unified query syntax**: YAML-based DSL with automatic translation
|
|
12
12
|
- **Reproducible searches**: Full session logging for PRISMA reporting
|
|
13
13
|
- **Resume support**: Continue interrupted searches at DB or page level
|
|
14
|
+
- **Fulltext management**: OA discovery, automatic retrieval, PMC XML to Markdown conversion
|
|
14
15
|
- **Reference manager integration**: Works with [reference-manager](https://github.com/ncukondo/reference-manager)
|
|
15
16
|
|
|
16
17
|
## Installation
|
|
@@ -114,12 +115,35 @@ Developing an effective search query is iterative. Start broad, then refine base
|
|
|
114
115
|
|
|
115
116
|
- **Keep query versions**: Save each iteration (v1, v2, v3) to track your development process and maintain reproducibility.
|
|
116
117
|
|
|
118
|
+
## Fulltext Retrieval
|
|
119
|
+
|
|
120
|
+
After screening, retrieve fulltext articles for included papers:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Check Open Access availability
|
|
124
|
+
search-hub fulltext check --session <session-id>
|
|
125
|
+
|
|
126
|
+
# Download available OA fulltexts (auto-converts PMC XML to Markdown)
|
|
127
|
+
search-hub fulltext fetch <session-id>
|
|
128
|
+
|
|
129
|
+
# For non-OA articles: create directories for manual download
|
|
130
|
+
search-hub fulltext init <session-id>
|
|
131
|
+
search-hub fulltext pending <session-id>
|
|
132
|
+
|
|
133
|
+
# After manually adding PDFs, sync and register
|
|
134
|
+
search-hub fulltext sync <session-id>
|
|
135
|
+
search-hub register <session-id>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
See [Fulltext Management Guide](./docs/fulltext.md) for details.
|
|
139
|
+
|
|
117
140
|
## Documentation
|
|
118
141
|
|
|
119
142
|
- [Query Guide](./docs/query-guide.md) - How to write query files
|
|
120
143
|
- [Command Reference](./docs/commands.md) - All CLI commands and options
|
|
121
144
|
- [Configuration](./docs/configuration.md) - Setup and configuration
|
|
122
145
|
- [Databases](./docs/databases.md) - Supported databases and tips
|
|
146
|
+
- [Fulltext Management](./docs/fulltext.md) - Fulltext retrieval and management
|
|
123
147
|
|
|
124
148
|
## Development
|
|
125
149
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAEpD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,IAAI,EAAE,MAAM,GACX,OAAO,CAeT;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,GACb,IAAI,CAcN;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAEpD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,IAAI,EAAE,MAAM,GACX,OAAO,CAeT;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,GACb,IAAI,CAcN;AAsCD;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAiBvE;AAoBD;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,GACZ,YAAY,CAiCd"}
|
|
@@ -43,6 +43,7 @@ function formatValue(value) {
|
|
|
43
43
|
if (value === null) return "null";
|
|
44
44
|
if (value === void 0) return "undefined";
|
|
45
45
|
if (typeof value === "string") return value;
|
|
46
|
+
if (Array.isArray(value)) return JSON.stringify(value);
|
|
46
47
|
if (typeof value === "object") return JSON.stringify(value, null, 2);
|
|
47
48
|
return String(value);
|
|
48
49
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sources":["../../../src/cli/commands/config.ts"],"sourcesContent":["/**\n * Config command implementation.\n *\n * Provides functionality to view and edit configuration values.\n */\nimport type { Config } from '../../config/index.js';\n\n/**\n * Result of a config operation.\n */\nexport interface ConfigResult {\n success: boolean;\n value?: string;\n error?: string;\n}\n\n/**\n * Get a nested value from an object using dot notation.\n *\n * @example\n * getNestedValue({ a: { b: 1 } }, 'a.b') // returns 1\n */\nexport function getNestedValue(\n obj: Record<string, unknown>,\n path: string\n): unknown {\n const keys = path.split('.');\n let current: unknown = obj;\n\n for (const key of keys) {\n if (current === null || current === undefined) {\n return undefined;\n }\n if (typeof current !== 'object') {\n return undefined;\n }\n current = (current as Record<string, unknown>)[key];\n }\n\n return current;\n}\n\n/**\n * Set a nested value in an object using dot notation.\n *\n * @example\n * setNestedValue({ a: { b: 1 } }, 'a.b', 2) // modifies obj to { a: { b: 2 } }\n */\nexport function setNestedValue(\n obj: Record<string, unknown>,\n path: string,\n value: unknown\n): void {\n const keys = path.split('.');\n let current = obj;\n\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i]!;\n if (!(key in current) || typeof current[key] !== 'object') {\n current[key] = {};\n }\n current = current[key] as Record<string, unknown>;\n }\n\n const lastKey = keys[keys.length - 1]!;\n current[lastKey] = value;\n}\n\n/**\n * Flatten a nested object into dot-notation keys.\n */\nfunction flattenObject(\n obj: Record<string, unknown>,\n prefix = ''\n): Array<{ key: string; value: unknown }> {\n const result: Array<{ key: string; value: unknown }> = [];\n\n for (const [key, value] of Object.entries(obj)) {\n const fullKey = prefix ? `${prefix}.${key}` : key;\n\n if (value !== null && typeof value === 'object' && !Array.isArray(value)) {\n result.push(\n ...flattenObject(value as Record<string, unknown>, fullKey)\n );\n } else {\n result.push({ key: fullKey, value });\n }\n }\n\n return result;\n}\n\n/**\n * Format a value for display.\n */\nfunction formatValue(value: unknown): string {\n if (value === null) return 'null';\n if (value === undefined) return 'undefined';\n if (typeof value === 'string') return value;\n if (typeof value === 'object') return JSON.stringify(value, null, 2);\n return String(value);\n}\n\n/**\n * View all configuration values.\n */\nexport function viewConfig(config: Config): string {\n const flattened = flattenObject(config as unknown as Record<string, unknown>);\n const lines = flattened.map(({ key, value }) => {\n const formattedValue = formatValue(value);\n return `${key} = ${formattedValue}`;\n });\n return lines.join('\\n');\n}\n\n/**\n * View a specific configuration key.\n */\nexport function viewConfigKey(config: Config, key: string): ConfigResult {\n const value = getNestedValue(\n config as unknown as Record<string, unknown>,\n key\n );\n\n if (value === undefined) {\n return {\n success: false,\n error: `Key \"${key}\" not found in configuration`,\n };\n }\n\n return {\n success: true,\n value: formatValue(value),\n };\n}\n\n/**\n * Parse a string value to its appropriate type.\n */\nfunction parseValue(value: string, existingValue: unknown): unknown {\n // Boolean\n if (value === 'true') return true;\n if (value === 'false') return false;\n\n // Number (if existing value is a number)\n if (typeof existingValue === 'number') {\n const num = Number(value);\n if (!isNaN(num)) return num;\n }\n\n // Default to string\n return value;\n}\n\n/**\n * Set a configuration key to a new value.\n * Only allows setting keys that already exist in the configuration.\n */\nexport function setConfigKey(\n config: Config,\n key: string,\n value: string\n): ConfigResult {\n if (!key) {\n return {\n success: false,\n error: 'Key cannot be empty',\n };\n }\n\n const existingValue = getNestedValue(\n config as unknown as Record<string, unknown>,\n key\n );\n\n // Reject unknown keys\n if (existingValue === undefined) {\n return {\n success: false,\n error: `Unknown configuration key: \"${key}\". Use \"search-hub config\" to see available keys.`,\n };\n }\n\n const parsedValue = parseValue(value, existingValue);\n\n setNestedValue(\n config as unknown as Record<string, unknown>,\n key,\n parsedValue\n );\n\n return {\n success: true,\n value: formatValue(parsedValue),\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"config.js","sources":["../../../src/cli/commands/config.ts"],"sourcesContent":["/**\n * Config command implementation.\n *\n * Provides functionality to view and edit configuration values.\n */\nimport type { Config } from '../../config/index.js';\n\n/**\n * Result of a config operation.\n */\nexport interface ConfigResult {\n success: boolean;\n value?: string;\n error?: string;\n}\n\n/**\n * Get a nested value from an object using dot notation.\n *\n * @example\n * getNestedValue({ a: { b: 1 } }, 'a.b') // returns 1\n */\nexport function getNestedValue(\n obj: Record<string, unknown>,\n path: string\n): unknown {\n const keys = path.split('.');\n let current: unknown = obj;\n\n for (const key of keys) {\n if (current === null || current === undefined) {\n return undefined;\n }\n if (typeof current !== 'object') {\n return undefined;\n }\n current = (current as Record<string, unknown>)[key];\n }\n\n return current;\n}\n\n/**\n * Set a nested value in an object using dot notation.\n *\n * @example\n * setNestedValue({ a: { b: 1 } }, 'a.b', 2) // modifies obj to { a: { b: 2 } }\n */\nexport function setNestedValue(\n obj: Record<string, unknown>,\n path: string,\n value: unknown\n): void {\n const keys = path.split('.');\n let current = obj;\n\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i]!;\n if (!(key in current) || typeof current[key] !== 'object') {\n current[key] = {};\n }\n current = current[key] as Record<string, unknown>;\n }\n\n const lastKey = keys[keys.length - 1]!;\n current[lastKey] = value;\n}\n\n/**\n * Flatten a nested object into dot-notation keys.\n */\nfunction flattenObject(\n obj: Record<string, unknown>,\n prefix = ''\n): Array<{ key: string; value: unknown }> {\n const result: Array<{ key: string; value: unknown }> = [];\n\n for (const [key, value] of Object.entries(obj)) {\n const fullKey = prefix ? `${prefix}.${key}` : key;\n\n if (value !== null && typeof value === 'object' && !Array.isArray(value)) {\n result.push(\n ...flattenObject(value as Record<string, unknown>, fullKey)\n );\n } else {\n result.push({ key: fullKey, value });\n }\n }\n\n return result;\n}\n\n/**\n * Format a value for display.\n */\nfunction formatValue(value: unknown): string {\n if (value === null) return 'null';\n if (value === undefined) return 'undefined';\n if (typeof value === 'string') return value;\n if (Array.isArray(value)) return JSON.stringify(value);\n if (typeof value === 'object') return JSON.stringify(value, null, 2);\n return String(value);\n}\n\n/**\n * View all configuration values.\n */\nexport function viewConfig(config: Config): string {\n const flattened = flattenObject(config as unknown as Record<string, unknown>);\n const lines = flattened.map(({ key, value }) => {\n const formattedValue = formatValue(value);\n return `${key} = ${formattedValue}`;\n });\n return lines.join('\\n');\n}\n\n/**\n * View a specific configuration key.\n */\nexport function viewConfigKey(config: Config, key: string): ConfigResult {\n const value = getNestedValue(\n config as unknown as Record<string, unknown>,\n key\n );\n\n if (value === undefined) {\n return {\n success: false,\n error: `Key \"${key}\" not found in configuration`,\n };\n }\n\n return {\n success: true,\n value: formatValue(value),\n };\n}\n\n/**\n * Parse a string value to its appropriate type.\n */\nfunction parseValue(value: string, existingValue: unknown): unknown {\n // Boolean\n if (value === 'true') return true;\n if (value === 'false') return false;\n\n // Number (if existing value is a number)\n if (typeof existingValue === 'number') {\n const num = Number(value);\n if (!isNaN(num)) return num;\n }\n\n // Default to string\n return value;\n}\n\n/**\n * Set a configuration key to a new value.\n * Only allows setting keys that already exist in the configuration.\n */\nexport function setConfigKey(\n config: Config,\n key: string,\n value: string\n): ConfigResult {\n if (!key) {\n return {\n success: false,\n error: 'Key cannot be empty',\n };\n }\n\n const existingValue = getNestedValue(\n config as unknown as Record<string, unknown>,\n key\n );\n\n // Reject unknown keys\n if (existingValue === undefined) {\n return {\n success: false,\n error: `Unknown configuration key: \"${key}\". Use \"search-hub config\" to see available keys.`,\n };\n }\n\n const parsedValue = parseValue(value, existingValue);\n\n setNestedValue(\n config as unknown as Record<string, unknown>,\n key,\n parsedValue\n );\n\n return {\n success: true,\n value: formatValue(parsedValue),\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FulltextAttachResult } from '../../../integration/types.js';
|
|
2
|
+
export interface FulltextAttachCommandOptions {
|
|
3
|
+
sessionDir: string;
|
|
4
|
+
dryRun: boolean;
|
|
5
|
+
onProgress?: (current: number, total: number) => void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Execute the standalone fulltext attach command.
|
|
9
|
+
* Reads the session's fulltext directories and attaches files to ref entries.
|
|
10
|
+
*/
|
|
11
|
+
export declare function executeFulltextAttach(options: FulltextAttachCommandOptions): Promise<FulltextAttachResult>;
|
|
12
|
+
//# sourceMappingURL=attach.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/attach.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAI1E,MAAM,WAAW,4BAA4B;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;AA8DD;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,oBAAoB,CAAC,CAgB/B"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { refExport, refFulltextAttach } from "../../../integration/ref-cli.js";
|
|
4
|
+
import { processFulltextEntries } from "../../../fulltext/attach-shared.js";
|
|
5
|
+
async function buildRefLookupFromLibrary(libraryPath, refCliOptions) {
|
|
6
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
7
|
+
try {
|
|
8
|
+
const content = await readFile(libraryPath, "utf-8");
|
|
9
|
+
const parsed = JSON.parse(content);
|
|
10
|
+
if (!Array.isArray(parsed)) {
|
|
11
|
+
console.warn("Warning: Reference library file is not a JSON array. Falling back to ref export.");
|
|
12
|
+
throw new Error("Not an array");
|
|
13
|
+
}
|
|
14
|
+
const entries = parsed;
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const id = entry["id"];
|
|
17
|
+
if (!id) continue;
|
|
18
|
+
const doi = entry["DOI"];
|
|
19
|
+
if (doi && typeof doi === "string") {
|
|
20
|
+
lookup.set(`doi:${doi}`, id);
|
|
21
|
+
}
|
|
22
|
+
const pmid = entry["PMID"];
|
|
23
|
+
if (pmid && typeof pmid === "string") {
|
|
24
|
+
lookup.set(`pmid:${pmid}`, id);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
try {
|
|
29
|
+
const entries = await refExport("*", refCliOptions);
|
|
30
|
+
if (Array.isArray(entries)) {
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const id = entry["id"];
|
|
33
|
+
if (!id) continue;
|
|
34
|
+
const doi = entry["DOI"];
|
|
35
|
+
if (doi && typeof doi === "string") {
|
|
36
|
+
lookup.set(`doi:${doi}`, id);
|
|
37
|
+
}
|
|
38
|
+
const pmid = entry["PMID"];
|
|
39
|
+
if (pmid && typeof pmid === "string") {
|
|
40
|
+
lookup.set(`pmid:${pmid}`, id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
console.warn('Warning: Could not read reference library. All articles will be skipped as "not_in_ref".');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return lookup;
|
|
49
|
+
}
|
|
50
|
+
async function executeFulltextAttach(options) {
|
|
51
|
+
const { sessionDir, dryRun, onProgress } = options;
|
|
52
|
+
const fulltextDir = join(sessionDir, "fulltext");
|
|
53
|
+
const libraryPath = join(sessionDir, "references.json");
|
|
54
|
+
const refCliOptions = { libraryPath };
|
|
55
|
+
const refLookup = await buildRefLookupFromLibrary(libraryPath, refCliOptions);
|
|
56
|
+
return processFulltextEntries({
|
|
57
|
+
fulltextDir,
|
|
58
|
+
refLookup,
|
|
59
|
+
attachFile: (refId, filePath) => refFulltextAttach(refId, filePath, refCliOptions),
|
|
60
|
+
dryRun,
|
|
61
|
+
...onProgress ? { onProgress } : {}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
executeFulltextAttach
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=attach.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.js","sources":["../../../../src/cli/commands/fulltext/attach.ts"],"sourcesContent":["/**\n * Standalone fulltext attach command.\n * Attaches fulltext files to existing reference-manager entries.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { FulltextAttachResult } from '../../../integration/types.js';\nimport { refFulltextAttach, refExport, type RefCliOptions } from '../../../integration/ref-cli.js';\nimport { processFulltextEntries } from '../../../fulltext/attach-shared.js';\n\nexport interface FulltextAttachCommandOptions {\n sessionDir: string;\n dryRun: boolean;\n onProgress?: (current: number, total: number) => void;\n}\n\n/**\n * Load the reference library and build a lookup map from identifiers to ref IDs.\n * Returns a map with keys like \"doi:10.1234/test\" and \"pmid:12345678\".\n */\nasync function buildRefLookupFromLibrary(\n libraryPath: string,\n refCliOptions: RefCliOptions,\n): Promise<Map<string, string>> {\n const lookup = new Map<string, string>();\n\n try {\n // Try to read library directly from JSON file\n const content = await readFile(libraryPath, 'utf-8');\n const parsed: unknown = JSON.parse(content);\n\n if (!Array.isArray(parsed)) {\n console.warn('Warning: Reference library file is not a JSON array. Falling back to ref export.');\n throw new Error('Not an array');\n }\n\n const entries = parsed as Array<Record<string, unknown>>;\n for (const entry of entries) {\n const id = entry['id'] as string;\n if (!id) continue;\n\n const doi = entry['DOI'];\n if (doi && typeof doi === 'string') {\n lookup.set(`doi:${doi}`, id);\n }\n const pmid = entry['PMID'];\n if (pmid && typeof pmid === 'string') {\n lookup.set(`pmid:${pmid}`, id);\n }\n }\n } catch {\n // If library doesn't exist or can't be read, try ref export\n try {\n const entries = await refExport('*', refCliOptions) as Array<Record<string, unknown>>;\n if (Array.isArray(entries)) {\n for (const entry of entries) {\n const id = entry['id'] as string;\n if (!id) continue;\n const doi = entry['DOI'];\n if (doi && typeof doi === 'string') {\n lookup.set(`doi:${doi}`, id);\n }\n const pmid = entry['PMID'];\n if (pmid && typeof pmid === 'string') {\n lookup.set(`pmid:${pmid}`, id);\n }\n }\n }\n } catch {\n console.warn('Warning: Could not read reference library. All articles will be skipped as \"not_in_ref\".');\n }\n }\n\n return lookup;\n}\n\n/**\n * Execute the standalone fulltext attach command.\n * Reads the session's fulltext directories and attaches files to ref entries.\n */\nexport async function executeFulltextAttach(\n options: FulltextAttachCommandOptions,\n): Promise<FulltextAttachResult> {\n const { sessionDir, dryRun, onProgress } = options;\n const fulltextDir = join(sessionDir, 'fulltext');\n const libraryPath = join(sessionDir, 'references.json');\n const refCliOptions: RefCliOptions = { libraryPath };\n\n // Build ref lookup from library\n const refLookup = await buildRefLookupFromLibrary(libraryPath, refCliOptions);\n\n return processFulltextEntries({\n fulltextDir,\n refLookup,\n attachFile: (refId, filePath) => refFulltextAttach(refId, filePath, refCliOptions),\n dryRun,\n ...(onProgress ? { onProgress } : {}),\n });\n}\n"],"names":[],"mappings":";;;;AAqBA,eAAe,0BACb,aACA,eAC8B;AAC9B,QAAM,6BAAa,IAAA;AAEnB,MAAI;AAEF,UAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,UAAM,SAAkB,KAAK,MAAM,OAAO;AAE1C,QAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC1B,cAAQ,KAAK,kFAAkF;AAC/F,YAAM,IAAI,MAAM,cAAc;AAAA,IAChC;AAEA,UAAM,UAAU;AAChB,eAAW,SAAS,SAAS;AAC3B,YAAM,KAAK,MAAM,IAAI;AACrB,UAAI,CAAC,GAAI;AAET,YAAM,MAAM,MAAM,KAAK;AACvB,UAAI,OAAO,OAAO,QAAQ,UAAU;AAClC,eAAO,IAAI,OAAO,GAAG,IAAI,EAAE;AAAA,MAC7B;AACA,YAAM,OAAO,MAAM,MAAM;AACzB,UAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,eAAO,IAAI,QAAQ,IAAI,IAAI,EAAE;AAAA,MAC/B;AAAA,IACF;AAAA,EACF,QAAQ;AAEN,QAAI;AACF,YAAM,UAAU,MAAM,UAAU,KAAK,aAAa;AAClD,UAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,mBAAW,SAAS,SAAS;AAC3B,gBAAM,KAAK,MAAM,IAAI;AACrB,cAAI,CAAC,GAAI;AACT,gBAAM,MAAM,MAAM,KAAK;AACvB,cAAI,OAAO,OAAO,QAAQ,UAAU;AAClC,mBAAO,IAAI,OAAO,GAAG,IAAI,EAAE;AAAA,UAC7B;AACA,gBAAM,OAAO,MAAM,MAAM;AACzB,cAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,mBAAO,IAAI,QAAQ,IAAI,IAAI,EAAE;AAAA,UAC/B;AAAA,QACF;AAAA,MACF;AAAA,IACF,QAAQ;AACN,cAAQ,KAAK,0FAA0F;AAAA,IACzG;AAAA,EACF;AAEA,SAAO;AACT;AAMA,eAAsB,sBACpB,SAC+B;AAC/B,QAAM,EAAE,YAAY,QAAQ,WAAA,IAAe;AAC3C,QAAM,cAAc,KAAK,YAAY,UAAU;AAC/C,QAAM,cAAc,KAAK,YAAY,iBAAiB;AACtD,QAAM,gBAA+B,EAAE,YAAA;AAGvC,QAAM,YAAY,MAAM,0BAA0B,aAAa,aAAa;AAE5E,SAAO,uBAAuB;AAAA,IAC5B;AAAA,IACA;AAAA,IACA,YAAY,CAAC,OAAO,aAAa,kBAAkB,OAAO,UAAU,aAAa;AAAA,IACjF;AAAA,IACA,GAAI,aAAa,EAAE,eAAe,CAAA;AAAA,EAAC,CACpC;AACH;"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DiscoveryConfig } from '../../../fulltext/discovery/index';
|
|
2
|
+
import { OAStatus } from '../../../fulltext/types';
|
|
3
|
+
export interface FulltextCheckOptions {
|
|
4
|
+
sessionDir: string;
|
|
5
|
+
config: DiscoveryConfig;
|
|
6
|
+
concurrency?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface FulltextCheckArticleResult {
|
|
9
|
+
doi?: string;
|
|
10
|
+
pmid?: string;
|
|
11
|
+
title: string;
|
|
12
|
+
oaStatus: OAStatus;
|
|
13
|
+
locationCount: number;
|
|
14
|
+
}
|
|
15
|
+
export interface FulltextCheckResult {
|
|
16
|
+
summary: {
|
|
17
|
+
total: number;
|
|
18
|
+
open: number;
|
|
19
|
+
closed: number;
|
|
20
|
+
unknown: number;
|
|
21
|
+
};
|
|
22
|
+
articles: FulltextCheckArticleResult[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Execute the fulltext check command.
|
|
26
|
+
* Checks OA availability for all included articles in a session,
|
|
27
|
+
* processing articles in parallel with a concurrency limit.
|
|
28
|
+
*/
|
|
29
|
+
export declare function executeFulltextCheck(options: FulltextCheckOptions): Promise<FulltextCheckResult>;
|
|
30
|
+
//# sourceMappingURL=check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/check.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAc,KAAK,eAAe,EAAyB,MAAM,mCAAmC,CAAC;AAE5G,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAMxD,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,eAAe,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,QAAQ,EAAE,0BAA0B,EAAE,CAAC;CACxC;AAoHD;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAiC9B"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFile, access, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
import { discoverOA } from "../../../fulltext/discovery/index.js";
|
|
5
|
+
import { loadMeta, saveMeta } from "../../../fulltext/meta.js";
|
|
6
|
+
const DEFAULT_CONCURRENCY = 3;
|
|
7
|
+
async function loadIncludedArticles(sessionDir) {
|
|
8
|
+
const reviewsPath = join(sessionDir, ".internal", "reviews.yaml");
|
|
9
|
+
const content = await readFile(reviewsPath, "utf-8");
|
|
10
|
+
const reviewFile = parse(content);
|
|
11
|
+
return reviewFile.articles.filter((a) => a.finalDecision === "include");
|
|
12
|
+
}
|
|
13
|
+
async function findArticleDir(sessionDir, article) {
|
|
14
|
+
const fulltextDir = join(sessionDir, "fulltext");
|
|
15
|
+
try {
|
|
16
|
+
await access(fulltextDir);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
let entries;
|
|
21
|
+
try {
|
|
22
|
+
const dirEntries = await readdir(fulltextDir);
|
|
23
|
+
entries = dirEntries.map(String);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
try {
|
|
29
|
+
const metaPath = join(fulltextDir, entry, "meta.json");
|
|
30
|
+
const meta = await loadMeta(metaPath);
|
|
31
|
+
if (article.doi && meta.doi === article.doi) return entry;
|
|
32
|
+
if (article.pmid && meta.pmid === article.pmid) return entry;
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
async function processArticle(article, sessionDir, config) {
|
|
39
|
+
const discoveryArticle = {};
|
|
40
|
+
if (article.doi) discoveryArticle.doi = article.doi;
|
|
41
|
+
if (article.pmid) discoveryArticle.pmid = article.pmid;
|
|
42
|
+
if (article.arxivId) discoveryArticle.arxivId = article.arxivId;
|
|
43
|
+
const discoveryResult = await discoverOA(discoveryArticle, config);
|
|
44
|
+
const articleResult = {
|
|
45
|
+
title: article.title,
|
|
46
|
+
oaStatus: discoveryResult.oaStatus,
|
|
47
|
+
locationCount: discoveryResult.locations.length
|
|
48
|
+
};
|
|
49
|
+
if (article.doi) articleResult.doi = article.doi;
|
|
50
|
+
if (article.pmid) articleResult.pmid = article.pmid;
|
|
51
|
+
const dirName = await findArticleDir(sessionDir, article);
|
|
52
|
+
if (dirName) {
|
|
53
|
+
try {
|
|
54
|
+
const metaPath = join(sessionDir, "fulltext", dirName, "meta.json");
|
|
55
|
+
const meta = await loadMeta(metaPath);
|
|
56
|
+
meta.oaStatus = discoveryResult.oaStatus;
|
|
57
|
+
meta.oaLocations = discoveryResult.locations;
|
|
58
|
+
meta.checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
59
|
+
await saveMeta(metaPath, meta);
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return articleResult;
|
|
64
|
+
}
|
|
65
|
+
async function runWithConcurrency(tasks, concurrency) {
|
|
66
|
+
const results = new Array(tasks.length);
|
|
67
|
+
let nextIndex = 0;
|
|
68
|
+
async function worker() {
|
|
69
|
+
while (nextIndex < tasks.length) {
|
|
70
|
+
const index = nextIndex++;
|
|
71
|
+
try {
|
|
72
|
+
const value = await tasks[index]();
|
|
73
|
+
results[index] = { status: "fulfilled", value };
|
|
74
|
+
} catch (reason) {
|
|
75
|
+
results[index] = { status: "rejected", reason };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker());
|
|
80
|
+
await Promise.all(workers);
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
async function executeFulltextCheck(options) {
|
|
84
|
+
const { sessionDir, config, concurrency = DEFAULT_CONCURRENCY } = options;
|
|
85
|
+
const articles = await loadIncludedArticles(sessionDir);
|
|
86
|
+
const summary = { total: articles.length, open: 0, closed: 0, unknown: 0 };
|
|
87
|
+
const tasks = articles.map(
|
|
88
|
+
(article) => () => processArticle(article, sessionDir, config)
|
|
89
|
+
);
|
|
90
|
+
const settled = await runWithConcurrency(tasks, concurrency);
|
|
91
|
+
const results = [];
|
|
92
|
+
for (const result of settled) {
|
|
93
|
+
if (result.status === "fulfilled") {
|
|
94
|
+
results.push(result.value);
|
|
95
|
+
switch (result.value.oaStatus) {
|
|
96
|
+
case "open":
|
|
97
|
+
summary.open++;
|
|
98
|
+
break;
|
|
99
|
+
case "closed":
|
|
100
|
+
summary.closed++;
|
|
101
|
+
break;
|
|
102
|
+
case "unknown":
|
|
103
|
+
summary.unknown++;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { summary, articles: results };
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
executeFulltextCheck
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.js","sources":["../../../../src/cli/commands/fulltext/check.ts"],"sourcesContent":["/**\n * Fulltext check command.\n * Checks OA availability for included articles in a session.\n */\n\nimport { readFile, readdir, access } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport { discoverOA, type DiscoveryConfig, type DiscoveryArticle } from '../../../fulltext/discovery/index';\nimport { loadMeta, saveMeta } from '../../../fulltext/meta';\nimport type { OAStatus } from '../../../fulltext/types';\nimport type { ReviewFile, ArticleEntry } from '../review/types';\n\n/** Default concurrency for parallel article processing */\nconst DEFAULT_CONCURRENCY = 3;\n\nexport interface FulltextCheckOptions {\n sessionDir: string;\n config: DiscoveryConfig;\n concurrency?: number;\n}\n\nexport interface FulltextCheckArticleResult {\n doi?: string;\n pmid?: string;\n title: string;\n oaStatus: OAStatus;\n locationCount: number;\n}\n\nexport interface FulltextCheckResult {\n summary: {\n total: number;\n open: number;\n closed: number;\n unknown: number;\n };\n articles: FulltextCheckArticleResult[];\n}\n\n/**\n * Load included articles from reviews.yaml\n */\nasync function loadIncludedArticles(sessionDir: string): Promise<ArticleEntry[]> {\n const reviewsPath = join(sessionDir, '.internal', 'reviews.yaml');\n const content = await readFile(reviewsPath, 'utf-8');\n const reviewFile = parseYaml(content) as ReviewFile;\n return reviewFile.articles.filter((a) => a.finalDecision === 'include');\n}\n\n/**\n * Try to find a meta.json matching an article in the fulltext directory.\n * Returns the dirName if found, null otherwise.\n */\nasync function findArticleDir(\n sessionDir: string,\n article: ArticleEntry\n): Promise<string | null> {\n const fulltextDir = join(sessionDir, 'fulltext');\n try {\n await access(fulltextDir);\n } catch {\n return null;\n }\n\n let entries: string[];\n try {\n const dirEntries = await readdir(fulltextDir);\n entries = dirEntries.map(String);\n } catch {\n return null;\n }\n\n for (const entry of entries) {\n try {\n const metaPath = join(fulltextDir, entry, 'meta.json');\n const meta = await loadMeta(metaPath);\n // Match by DOI or PMID\n if (article.doi && meta.doi === article.doi) return entry;\n if (article.pmid && meta.pmid === article.pmid) return entry;\n } catch {\n // Skip entries without valid meta.json\n }\n }\n return null;\n}\n\n/**\n * Process a single article: run OA discovery and optionally update meta.json.\n */\nasync function processArticle(\n article: ArticleEntry,\n sessionDir: string,\n config: DiscoveryConfig\n): Promise<FulltextCheckArticleResult> {\n const discoveryArticle: DiscoveryArticle = {};\n if (article.doi) discoveryArticle.doi = article.doi;\n if (article.pmid) discoveryArticle.pmid = article.pmid;\n if (article.arxivId) discoveryArticle.arxivId = article.arxivId;\n const discoveryResult = await discoverOA(discoveryArticle, config);\n\n const articleResult: FulltextCheckArticleResult = {\n title: article.title,\n oaStatus: discoveryResult.oaStatus,\n locationCount: discoveryResult.locations.length,\n };\n if (article.doi) articleResult.doi = article.doi;\n if (article.pmid) articleResult.pmid = article.pmid;\n\n // Try to update meta.json if a fulltext directory exists for this article\n const dirName = await findArticleDir(sessionDir, article);\n if (dirName) {\n try {\n const metaPath = join(sessionDir, 'fulltext', dirName, 'meta.json');\n const meta = await loadMeta(metaPath);\n meta.oaStatus = discoveryResult.oaStatus;\n meta.oaLocations = discoveryResult.locations;\n meta.checkedAt = new Date().toISOString();\n await saveMeta(metaPath, meta);\n } catch {\n // Meta update is best-effort\n }\n }\n\n return articleResult;\n}\n\n/**\n * Run async tasks with a concurrency limit.\n */\nasync function runWithConcurrency<T>(\n tasks: Array<() => Promise<T>>,\n concurrency: number\n): Promise<PromiseSettledResult<T>[]> {\n const results: PromiseSettledResult<T>[] = new Array(tasks.length);\n let nextIndex = 0;\n\n async function worker(): Promise<void> {\n while (nextIndex < tasks.length) {\n const index = nextIndex++;\n try {\n const value = await tasks[index]!();\n results[index] = { status: 'fulfilled', value };\n } catch (reason) {\n results[index] = { status: 'rejected', reason };\n }\n }\n }\n\n const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker());\n await Promise.all(workers);\n return results;\n}\n\n/**\n * Execute the fulltext check command.\n * Checks OA availability for all included articles in a session,\n * processing articles in parallel with a concurrency limit.\n */\nexport async function executeFulltextCheck(\n options: FulltextCheckOptions\n): Promise<FulltextCheckResult> {\n const { sessionDir, config, concurrency = DEFAULT_CONCURRENCY } = options;\n\n // Load included articles\n const articles = await loadIncludedArticles(sessionDir);\n\n const summary = { total: articles.length, open: 0, closed: 0, unknown: 0 };\n\n const tasks = articles.map(\n (article) => () => processArticle(article, sessionDir, config)\n );\n\n const settled = await runWithConcurrency(tasks, concurrency);\n\n const results: FulltextCheckArticleResult[] = [];\n for (const result of settled) {\n if (result.status === 'fulfilled') {\n results.push(result.value);\n switch (result.value.oaStatus) {\n case 'open':\n summary.open++;\n break;\n case 'closed':\n summary.closed++;\n break;\n case 'unknown':\n summary.unknown++;\n break;\n }\n }\n }\n\n return { summary, articles: results };\n}\n"],"names":["parseYaml"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fulltext convert command - converts PMC XML to Markdown.
|
|
3
|
+
*/
|
|
4
|
+
export interface FulltextConvertOptions {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
article?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ConvertArticleResult {
|
|
9
|
+
dirName: string;
|
|
10
|
+
title: string;
|
|
11
|
+
status: 'converted' | 'skipped' | 'failed';
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ConvertCommandResult {
|
|
15
|
+
success: boolean;
|
|
16
|
+
converted: number;
|
|
17
|
+
skipped: number;
|
|
18
|
+
failed: number;
|
|
19
|
+
articles: ConvertArticleResult[];
|
|
20
|
+
}
|
|
21
|
+
export declare function executeFulltextConvert(options: FulltextConvertOptions, sessionsDir: string): Promise<ConvertCommandResult>;
|
|
22
|
+
//# sourceMappingURL=convert.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/convert.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,oBAAoB,EAAE,CAAC;CAClC;AAwCD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,sBAAsB,EAC/B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,CAAC,CA6D/B"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getArticleDir, getMetaPath, getFulltextDir } from "../../../fulltext/paths.js";
|
|
4
|
+
import { convertPmcXmlToMarkdown } from "../../../fulltext/convert/index.js";
|
|
5
|
+
async function fileExists(path) {
|
|
6
|
+
try {
|
|
7
|
+
await stat(path);
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function getArticleDirs(sessionDir, articleFilter) {
|
|
14
|
+
const fulltextDir = getFulltextDir(sessionDir);
|
|
15
|
+
if (articleFilter) {
|
|
16
|
+
const articlePath = getArticleDir(sessionDir, articleFilter);
|
|
17
|
+
if (await fileExists(articlePath)) {
|
|
18
|
+
return [articleFilter];
|
|
19
|
+
}
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const entries = await readdir(fulltextDir, { withFileTypes: true });
|
|
24
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function executeFulltextConvert(options, sessionsDir) {
|
|
30
|
+
const sessionDir = join(sessionsDir, options.sessionId);
|
|
31
|
+
const articleDirs = await getArticleDirs(sessionDir, options.article);
|
|
32
|
+
const articles = [];
|
|
33
|
+
let converted = 0;
|
|
34
|
+
let skipped = 0;
|
|
35
|
+
let failed = 0;
|
|
36
|
+
for (const dirName of articleDirs) {
|
|
37
|
+
const articleDir = getArticleDir(sessionDir, dirName);
|
|
38
|
+
const xmlPath = join(articleDir, "fulltext.xml");
|
|
39
|
+
const mdPath = join(articleDir, "fulltext.md");
|
|
40
|
+
const metaPath = getMetaPath(sessionDir, dirName);
|
|
41
|
+
if (!await fileExists(xmlPath)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (await fileExists(mdPath)) {
|
|
45
|
+
skipped++;
|
|
46
|
+
articles.push({ dirName, title: dirName, status: "skipped" });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const metaPathExists = await fileExists(metaPath);
|
|
50
|
+
const result = await convertPmcXmlToMarkdown(
|
|
51
|
+
xmlPath,
|
|
52
|
+
mdPath,
|
|
53
|
+
metaPathExists ? metaPath : void 0
|
|
54
|
+
);
|
|
55
|
+
if (result.success) {
|
|
56
|
+
converted++;
|
|
57
|
+
articles.push({
|
|
58
|
+
dirName,
|
|
59
|
+
title: result.title ?? dirName,
|
|
60
|
+
status: "converted"
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
failed++;
|
|
64
|
+
const articleResult = {
|
|
65
|
+
dirName,
|
|
66
|
+
title: dirName,
|
|
67
|
+
status: "failed"
|
|
68
|
+
};
|
|
69
|
+
if (result.error) articleResult.error = result.error;
|
|
70
|
+
articles.push(articleResult);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
success: failed === 0,
|
|
75
|
+
converted,
|
|
76
|
+
skipped,
|
|
77
|
+
failed,
|
|
78
|
+
articles
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
executeFulltextConvert
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=convert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convert.js","sources":["../../../../src/cli/commands/fulltext/convert.ts"],"sourcesContent":["/**\n * Fulltext convert command - converts PMC XML to Markdown.\n */\n\nimport { readdir, stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { getFulltextDir, getArticleDir, getMetaPath } from '../../../fulltext/paths.js';\nimport { convertPmcXmlToMarkdown } from '../../../fulltext/convert/index.js';\n\nexport interface FulltextConvertOptions {\n sessionId: string;\n article?: string;\n}\n\nexport interface ConvertArticleResult {\n dirName: string;\n title: string;\n status: 'converted' | 'skipped' | 'failed';\n error?: string;\n}\n\nexport interface ConvertCommandResult {\n success: boolean;\n converted: number;\n skipped: number;\n failed: number;\n articles: ConvertArticleResult[];\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Get list of article directory names to process.\n */\nasync function getArticleDirs(\n sessionDir: string,\n articleFilter?: string,\n): Promise<string[]> {\n const fulltextDir = getFulltextDir(sessionDir);\n\n if (articleFilter) {\n // Filter to specific article\n const articlePath = getArticleDir(sessionDir, articleFilter);\n if (await fileExists(articlePath)) {\n return [articleFilter];\n }\n return [];\n }\n\n // List all directories in fulltext/\n try {\n const entries = await readdir(fulltextDir, { withFileTypes: true });\n return entries\n .filter((e) => e.isDirectory())\n .map((e) => e.name);\n } catch {\n return [];\n }\n}\n\nexport async function executeFulltextConvert(\n options: FulltextConvertOptions,\n sessionsDir: string,\n): Promise<ConvertCommandResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n const articleDirs = await getArticleDirs(sessionDir, options.article);\n\n const articles: ConvertArticleResult[] = [];\n let converted = 0;\n let skipped = 0;\n let failed = 0;\n\n for (const dirName of articleDirs) {\n const articleDir = getArticleDir(sessionDir, dirName);\n const xmlPath = join(articleDir, 'fulltext.xml');\n const mdPath = join(articleDir, 'fulltext.md');\n const metaPath = getMetaPath(sessionDir, dirName);\n\n // Check if XML exists\n if (!(await fileExists(xmlPath))) {\n continue; // No XML to convert, skip silently\n }\n\n // Check if already converted\n if (await fileExists(mdPath)) {\n skipped++;\n articles.push({ dirName, title: dirName, status: 'skipped' });\n continue;\n }\n\n // Convert\n const metaPathExists = await fileExists(metaPath);\n const result = await convertPmcXmlToMarkdown(\n xmlPath,\n mdPath,\n metaPathExists ? metaPath : undefined,\n );\n\n if (result.success) {\n converted++;\n articles.push({\n dirName,\n title: result.title ?? dirName,\n status: 'converted',\n });\n } else {\n failed++;\n const articleResult: ConvertArticleResult = {\n dirName,\n title: dirName,\n status: 'failed',\n };\n if (result.error) articleResult.error = result.error;\n articles.push(articleResult);\n }\n }\n\n return {\n success: failed === 0,\n converted,\n skipped,\n failed,\n articles,\n };\n}\n"],"names":[],"mappings":";;;;AA6BA,eAAe,WAAW,MAAgC;AACxD,MAAI;AACF,UAAM,KAAK,IAAI;AACf,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAe,eACb,YACA,eACmB;AACnB,QAAM,cAAc,eAAe,UAAU;AAE7C,MAAI,eAAe;AAEjB,UAAM,cAAc,cAAc,YAAY,aAAa;AAC3D,QAAI,MAAM,WAAW,WAAW,GAAG;AACjC,aAAO,CAAC,aAAa;AAAA,IACvB;AACA,WAAO,CAAA;AAAA,EACT;AAGA,MAAI;AACF,UAAM,UAAU,MAAM,QAAQ,aAAa,EAAE,eAAe,MAAM;AAClE,WAAO,QACJ,OAAO,CAAC,MAAM,EAAE,aAAa,EAC7B,IAAI,CAAC,MAAM,EAAE,IAAI;AAAA,EACtB,QAAQ;AACN,WAAO,CAAA;AAAA,EACT;AACF;AAEA,eAAsB,uBACpB,SACA,aAC+B;AAC/B,QAAM,aAAa,KAAK,aAAa,QAAQ,SAAS;AACtD,QAAM,cAAc,MAAM,eAAe,YAAY,QAAQ,OAAO;AAEpE,QAAM,WAAmC,CAAA;AACzC,MAAI,YAAY;AAChB,MAAI,UAAU;AACd,MAAI,SAAS;AAEb,aAAW,WAAW,aAAa;AACjC,UAAM,aAAa,cAAc,YAAY,OAAO;AACpD,UAAM,UAAU,KAAK,YAAY,cAAc;AAC/C,UAAM,SAAS,KAAK,YAAY,aAAa;AAC7C,UAAM,WAAW,YAAY,YAAY,OAAO;AAGhD,QAAI,CAAE,MAAM,WAAW,OAAO,GAAI;AAChC;AAAA,IACF;AAGA,QAAI,MAAM,WAAW,MAAM,GAAG;AAC5B;AACA,eAAS,KAAK,EAAE,SAAS,OAAO,SAAS,QAAQ,WAAW;AAC5D;AAAA,IACF;AAGA,UAAM,iBAAiB,MAAM,WAAW,QAAQ;AAChD,UAAM,SAAS,MAAM;AAAA,MACnB;AAAA,MACA;AAAA,MACA,iBAAiB,WAAW;AAAA,IAAA;AAG9B,QAAI,OAAO,SAAS;AAClB;AACA,eAAS,KAAK;AAAA,QACZ;AAAA,QACA,OAAO,OAAO,SAAS;AAAA,QACvB,QAAQ;AAAA,MAAA,CACT;AAAA,IACH,OAAO;AACL;AACA,YAAM,gBAAsC;AAAA,QAC1C;AAAA,QACA,OAAO;AAAA,QACP,QAAQ;AAAA,MAAA;AAEV,UAAI,OAAO,MAAO,eAAc,QAAQ,OAAO;AAC/C,eAAS,KAAK,aAAa;AAAA,IAC7B;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS,WAAW;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fulltext fetch command - downloads OA fulltexts for session articles.
|
|
3
|
+
*/
|
|
4
|
+
export interface FulltextFetchOptions {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
sessionsDir: string;
|
|
7
|
+
source?: string[];
|
|
8
|
+
convertMarkdown?: boolean;
|
|
9
|
+
dryRun?: boolean;
|
|
10
|
+
concurrency?: number;
|
|
11
|
+
retryDelay?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface FulltextFetchArticle {
|
|
14
|
+
dirName: string;
|
|
15
|
+
title: string;
|
|
16
|
+
oaStatus: string;
|
|
17
|
+
locationCount: number;
|
|
18
|
+
}
|
|
19
|
+
export interface FulltextFetchResult {
|
|
20
|
+
summary: {
|
|
21
|
+
total: number;
|
|
22
|
+
downloaded: number;
|
|
23
|
+
failed: number;
|
|
24
|
+
skipped: number;
|
|
25
|
+
};
|
|
26
|
+
articles: FulltextFetchArticle[];
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Execute the fulltext fetch command.
|
|
31
|
+
* Loads articles from reviews.yaml, checks OA status via meta.json,
|
|
32
|
+
* and downloads PDFs/XMLs from OA sources.
|
|
33
|
+
*/
|
|
34
|
+
export declare function executeFulltextFetch(options: FulltextFetchOptions): Promise<FulltextFetchResult>;
|
|
35
|
+
//# sourceMappingURL=fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/fulltext/fetch.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,QAAQ,EAAE,oBAAoB,EAAE,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CA8G9B"}
|