@ncukondo/search-hub 0.19.0 → 0.20.1

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 (82) hide show
  1. package/dist/cli/commands/register.d.ts +8 -0
  2. package/dist/cli/commands/register.d.ts.map +1 -1
  3. package/dist/cli/commands/register.js +9 -0
  4. package/dist/cli/commands/register.js.map +1 -1
  5. package/dist/cli/commands/related.d.ts +66 -0
  6. package/dist/cli/commands/related.d.ts.map +1 -0
  7. package/dist/cli/commands/related.js +161 -0
  8. package/dist/cli/commands/related.js.map +1 -0
  9. package/dist/cli/commands/review/extract.d.ts.map +1 -1
  10. package/dist/cli/commands/review/extract.js +15 -5
  11. package/dist/cli/commands/review/extract.js.map +1 -1
  12. package/dist/cli/commands/review/init.d.ts +2 -3
  13. package/dist/cli/commands/review/init.d.ts.map +1 -1
  14. package/dist/cli/commands/review/init.js +1 -0
  15. package/dist/cli/commands/review/init.js.map +1 -1
  16. package/dist/cli/commands/review/next-steps.d.ts +3 -0
  17. package/dist/cli/commands/review/next-steps.d.ts.map +1 -1
  18. package/dist/cli/commands/review/next-steps.js +53 -19
  19. package/dist/cli/commands/review/next-steps.js.map +1 -1
  20. package/dist/cli/commands/review/schema.d.ts +8 -0
  21. package/dist/cli/commands/review/schema.d.ts.map +1 -1
  22. package/dist/cli/commands/review/schema.js +3 -0
  23. package/dist/cli/commands/review/schema.js.map +1 -1
  24. package/dist/cli/commands/review/status.d.ts +3 -1
  25. package/dist/cli/commands/review/status.d.ts.map +1 -1
  26. package/dist/cli/commands/review/status.js +3 -1
  27. package/dist/cli/commands/review/status.js.map +1 -1
  28. package/dist/cli/commands/review/types.d.ts +2 -1
  29. package/dist/cli/commands/review/types.d.ts.map +1 -1
  30. package/dist/cli/commands/review/types.js.map +1 -1
  31. package/dist/cli/commands/search-executor.d.ts.map +1 -1
  32. package/dist/cli/commands/search-executor.js +3 -2
  33. package/dist/cli/commands/search-executor.js.map +1 -1
  34. package/dist/cli/commands/search.d.ts +2 -0
  35. package/dist/cli/commands/search.d.ts.map +1 -1
  36. package/dist/cli/commands/search.js +3 -0
  37. package/dist/cli/commands/search.js.map +1 -1
  38. package/dist/cli/index.d.ts.map +1 -1
  39. package/dist/cli/index.js +133 -5
  40. package/dist/cli/index.js.map +1 -1
  41. package/dist/cli/suggestions/rules.d.ts.map +1 -1
  42. package/dist/cli/suggestions/rules.js +19 -3
  43. package/dist/cli/suggestions/rules.js.map +1 -1
  44. package/dist/node_modules/dom-serializer/lib/index.js +1 -1
  45. package/dist/node_modules/domelementtype/lib/index.js +1 -1
  46. package/dist/node_modules/nth-check/lib/index.js +1 -1
  47. package/dist/providers/arxiv/provider.d.ts.map +1 -1
  48. package/dist/providers/arxiv/provider.js +7 -4
  49. package/dist/providers/arxiv/provider.js.map +1 -1
  50. package/dist/providers/base/types.d.ts +8 -0
  51. package/dist/providers/base/types.d.ts.map +1 -1
  52. package/dist/providers/base/types.js.map +1 -1
  53. package/dist/providers/eric/provider.d.ts +3 -0
  54. package/dist/providers/eric/provider.d.ts.map +1 -1
  55. package/dist/providers/eric/provider.js +11 -0
  56. package/dist/providers/eric/provider.js.map +1 -1
  57. package/dist/providers/pubmed/client.d.ts +15 -1
  58. package/dist/providers/pubmed/client.d.ts.map +1 -1
  59. package/dist/providers/pubmed/client.js +64 -1
  60. package/dist/providers/pubmed/client.js.map +1 -1
  61. package/dist/providers/pubmed/index.d.ts +2 -2
  62. package/dist/providers/pubmed/index.d.ts.map +1 -1
  63. package/dist/providers/pubmed/parser.d.ts +8 -1
  64. package/dist/providers/pubmed/parser.d.ts.map +1 -1
  65. package/dist/providers/pubmed/parser.js +23 -1
  66. package/dist/providers/pubmed/parser.js.map +1 -1
  67. package/dist/providers/pubmed/provider.d.ts.map +1 -1
  68. package/dist/providers/pubmed/provider.js +8 -2
  69. package/dist/providers/pubmed/provider.js.map +1 -1
  70. package/dist/providers/pubmed/types.d.ts +29 -0
  71. package/dist/providers/pubmed/types.d.ts.map +1 -1
  72. package/dist/providers/scopus/client.d.ts +2 -0
  73. package/dist/providers/scopus/client.d.ts.map +1 -1
  74. package/dist/providers/scopus/client.js +3 -0
  75. package/dist/providers/scopus/client.js.map +1 -1
  76. package/dist/providers/scopus/provider.d.ts.map +1 -1
  77. package/dist/providers/scopus/provider.js +7 -1
  78. package/dist/providers/scopus/provider.js.map +1 -1
  79. package/dist/session/types.d.ts +13 -1
  80. package/dist/session/types.d.ts.map +1 -1
  81. package/dist/session/types.js.map +1 -1
  82. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/search.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/search.ts"],"names":[],"mappings":"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"}
@@ -32,6 +32,9 @@ function parseSearchOptions(queryFile, options) {
32
32
  if (options.strict) {
33
33
  result.strict = true;
34
34
  }
35
+ if (options.sort === "relevance" || options.sort === "date") {
36
+ result.sort = options.sort;
37
+ }
35
38
  return result;
36
39
  }
37
40
  function validateSearchInput(options) {
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","sources":["../../../src/cli/commands/search.ts"],"sourcesContent":["import type { ProviderName } from '../../session/types.js';\nimport type { ConnectionTestResult } from '../../providers/base/types.js';\nimport type { Config } from '../../config/index.js';\nimport { parseProviderNames } from '../utils/validation.js';\nimport { createProviderInstance } from './search-executor.js';\n\nexport interface SearchCommandOptions {\n queryFile?: string;\n directQuery?: string;\n providers?: ProviderName[];\n sessionName?: string;\n maxResults?: number;\n dryRun?: boolean;\n countOnly?: boolean;\n preview?: boolean;\n noResume?: boolean;\n strict?: boolean;\n}\n\nexport interface CommandLineOptions {\n db?: string | undefined;\n query?: string | undefined;\n name?: string | undefined;\n maxResults?: string | undefined;\n dryRun?: boolean | undefined;\n countOnly?: boolean | undefined;\n preview?: boolean | undefined;\n noResume?: boolean | undefined;\n strict?: boolean | undefined;\n}\n\nexport interface TranslationResult {\n provider: string;\n query: string;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\n/**\n * Options for enhanced dry-run output.\n */\nexport interface DryRunOutputOptions {\n config?: Config;\n providers?: ProviderName[];\n skipConnectionTest?: boolean | undefined;\n}\n\nexport function parseSearchOptions(\n queryFile: string | undefined,\n options: CommandLineOptions\n): SearchCommandOptions {\n const result: SearchCommandOptions = {};\n\n if (queryFile) {\n result.queryFile = queryFile;\n }\n\n if (options.query) {\n result.directQuery = options.query;\n }\n\n if (options.db) {\n result.providers = parseProviderNames(options.db);\n }\n\n if (options.name) {\n result.sessionName = options.name;\n }\n\n if (options.maxResults) {\n result.maxResults = parseInt(options.maxResults, 10);\n }\n\n if (options.dryRun) {\n result.dryRun = true;\n }\n\n if (options.countOnly) {\n result.countOnly = true;\n }\n\n if (options.preview) {\n result.preview = true;\n }\n\n if (options.noResume) {\n result.noResume = true;\n }\n\n if (options.strict) {\n result.strict = true;\n }\n\n return result;\n}\n\nexport function validateSearchInput(options: SearchCommandOptions): ValidationResult {\n if (!options.queryFile && !options.directQuery) {\n return {\n valid: false,\n error: 'Either a query file or --query option is required',\n };\n }\n\n if (options.directQuery && (!options.providers || options.providers.length === 0)) {\n return {\n valid: false,\n error: 'Direct query (--query) requires --db option to specify the provider',\n };\n }\n\n if (options.directQuery && options.providers && options.providers.length > 1) {\n return {\n valid: false,\n error: 'Direct query (--query) can only be used with a single provider (--db)',\n };\n }\n\n if (options.preview && options.countOnly) {\n return {\n valid: false,\n error: '--preview and --count-only cannot be used together',\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Test provider connections and return results.\n */\nexport async function testProviderConnections(\n providers: ProviderName[],\n config: Config\n): Promise<Record<string, ConnectionTestResult>> {\n const results: Record<string, ConnectionTestResult> = {};\n await Promise.all(\n providers.map(async (name) => {\n const provider = createProviderInstance(name, config);\n if (!provider) {\n results[name] = { ok: false, error: 'Provider configuration incomplete' };\n return;\n }\n results[name] = await provider.testConnection();\n })\n );\n return results;\n}\n\n/**\n * Format provider readiness summary for dry-run output.\n */\nexport function formatProviderReadiness(\n providers: ProviderName[],\n config: Config,\n connectionResults?: Record<string, ConnectionTestResult>\n): string {\n const lines: string[] = [];\n lines.push('Provider readiness:');\n for (const provider of providers) {\n const providerConfig = config.providers[provider];\n const connResult = connectionResults?.[provider];\n const status = getProviderStatus(provider, providerConfig, connResult);\n const mark = status.ready ? '✓' : '✗';\n lines.push(` ${mark} ${provider.padEnd(10)}${status.message}`);\n }\n return lines.join('\\n');\n}\n\nfunction getProviderStatus(\n provider: ProviderName,\n providerConfig: { email?: string; api_key?: string },\n connectionResult?: ConnectionTestResult\n): { ready: boolean; message: string } {\n switch (provider) {\n case 'pubmed': {\n if (connectionResult && !connectionResult.ok) {\n return { ready: false, message: `not ready (${connectionResult.error})` };\n }\n if (!providerConfig.email) {\n return { ready: true, message: connectionResult ? 'ready (verified, email: not configured (recommended))' : 'ready (email: not configured (recommended))' };\n }\n return { ready: true, message: connectionResult ? 'ready (verified, email: configured)' : 'ready (email: configured)' };\n }\n case 'scopus': {\n if (!providerConfig.api_key) {\n return { ready: false, message: 'missing api_key (required)' };\n }\n if (connectionResult && !connectionResult.ok) {\n return { ready: false, message: `not ready (${connectionResult.error})` };\n }\n return { ready: true, message: connectionResult ? 'ready (verified)' : 'ready' };\n }\n case 'eric':\n case 'arxiv': {\n if (connectionResult && !connectionResult.ok) {\n return { ready: false, message: `not ready (${connectionResult.error})` };\n }\n return { ready: true, message: connectionResult ? 'ready (verified)' : 'ready' };\n }\n default:\n return { ready: true, message: 'ready' };\n }\n}\n\n/**\n * Format query diagnostics warnings for dry-run output.\n */\nexport function formatQueryDiagnostics(translations: TranslationResult[]): string {\n const warnings: string[] = [];\n for (const t of translations) {\n if (t.provider === 'pubmed') {\n if (/\\bNOT\\b/.test(t.query)) {\n warnings.push(' ⚠ pubmed: query uses NOT operator (ensure correct syntax for exclusions)');\n }\n if (/\\*\\[(?:mh|mesh)\\]/i.test(t.query)) {\n warnings.push(' ⚠ pubmed: wildcard in MeSH term — PubMed does not support wildcards in MeSH fields');\n }\n }\n }\n if (warnings.length === 0) {\n return '';\n }\n const lines: string[] = [];\n lines.push('Diagnostics:');\n lines.push(...warnings);\n return lines.join('\\n');\n}\n\n/**\n * Result of a count-only query for a single provider.\n */\nexport interface CountResult {\n provider: string;\n count: number;\n error?: string;\n}\n\n\n/**\n * Preview result for a single provider\n */\nexport interface PreviewResult {\n provider: string;\n count: number;\n titles: string[];\n error?: string;\n}\n\n/**\n * Format count-only output for display.\n */\nexport function formatCountOnlyOutput(\n counts: CountResult[],\n queryLabel?: string\n): string {\n const label = queryLabel ?? 'direct-query';\n const lines: string[] = [];\n\n lines.push(`Query: ${label} (count only)`);\n lines.push('');\n\n // Find the max provider name length for alignment (including colon)\n const maxNameLen = Math.max(...counts.map((c) => c.provider.length + 1), 6);\n\n // Calculate total (excluding errors)\n let total = 0;\n\n for (const c of counts) {\n if (c.error) {\n lines.push(` ${(c.provider + ':').padEnd(maxNameLen)} error: ${c.error}`);\n } else {\n const countStr = String(c.count).padStart(6);\n lines.push(` ${(c.provider + ':').padEnd(maxNameLen)} ${countStr} hits`);\n total += c.count;\n }\n }\n\n // Separator and total\n const separatorLen = maxNameLen + 14;\n lines.push(` ${'─'.repeat(separatorLen)}`);\n const totalStr = String(total).padStart(6);\n lines.push(` ${('total:').padEnd(maxNameLen)} ${totalStr} hits (before deduplication)`);\n\n return lines.join('\\n');\n}\n\n\n/**\n * Format preview output showing counts and sample titles.\n */\nexport function formatPreviewOutput(\n results: PreviewResult[],\n queryLabel?: string\n): string {\n const label = queryLabel ?? 'direct-query';\n const lines: string[] = [];\n\n lines.push(`Query: ${label} (preview)`);\n lines.push('');\n\n // Find the max provider name length for alignment (including colon)\n const maxNameLen = Math.max(...results.map((r) => r.provider.length + 1), 6);\n\n // Calculate total (excluding errors)\n let total = 0;\n\n for (const r of results) {\n if (r.error) {\n lines.push(` ${(r.provider + ':').padEnd(maxNameLen)} error: ${r.error}`);\n } else {\n const countStr = String(r.count).padStart(6);\n lines.push(` ${(r.provider + ':').padEnd(maxNameLen)} ${countStr} hits`);\n total += r.count;\n\n // Show sample titles\n if (r.titles.length > 0) {\n for (const title of r.titles) {\n lines.push(` • ${title}`);\n }\n }\n }\n lines.push('');\n }\n\n // Separator and total\n const separatorLen = maxNameLen + 14;\n lines.push(` ${'─'.repeat(separatorLen)}`);\n const totalStr = String(total).padStart(6);\n lines.push(` ${('total:').padEnd(maxNameLen)} ${totalStr} hits (before deduplication)`);\n\n return lines.join('\\n');\n}\n\nexport async function formatDryRunOutput(\n translations: TranslationResult[],\n options?: DryRunOutputOptions\n): Promise<string> {\n if (translations.length === 0) {\n return 'No translations available.';\n }\n const sections: string[] = [];\n if (options?.config && options?.providers) {\n let connectionResults: Record<string, ConnectionTestResult> | undefined;\n if (!options.skipConnectionTest) {\n connectionResults = await testProviderConnections(options.providers, options.config);\n }\n sections.push(formatProviderReadiness(options.providers, options.config, connectionResults));\n sections.push('');\n }\n const queryLines: string[] = [];\n queryLines.push('Translated queries:');\n queryLines.push('');\n for (const t of translations) {\n queryLines.push(`[${t.provider}]`);\n queryLines.push(t.query);\n queryLines.push('');\n }\n sections.push(queryLines.join('\\n'));\n const diagnostics = formatQueryDiagnostics(translations);\n if (diagnostics) {\n sections.push(diagnostics);\n sections.push('');\n }\n return sections.join('\\n');\n}\n\n\n/**\n * Format warning for short keywords that may cause noisy results.\n */\nexport function formatShortKeywordWarning(shortKeywords: string[]): string {\n if (shortKeywords.length === 0) {\n return '';\n }\n\n const keywordList = shortKeywords.join(', ');\n return `⚠ Query contains short keywords: ${keywordList}\n Short terms may match unrelated acronyms. Consider:\n - Adding full phrases (e.g., \"Objective Structured Clinical Examination\")\n - Using exclude terms to filter false matches`;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"search.js","sources":["../../../src/cli/commands/search.ts"],"sourcesContent":["import type { ProviderName } from '../../session/types.js';\nimport type { ConnectionTestResult } from '../../providers/base/types.js';\nimport type { Config } from '../../config/index.js';\nimport { parseProviderNames } from '../utils/validation.js';\nimport { createProviderInstance } from './search-executor.js';\n\nexport interface SearchCommandOptions {\n queryFile?: string;\n directQuery?: string;\n providers?: ProviderName[];\n sessionName?: string;\n maxResults?: number;\n sort?: 'relevance' | 'date';\n dryRun?: boolean;\n countOnly?: boolean;\n preview?: boolean;\n noResume?: boolean;\n strict?: boolean;\n}\n\nexport interface CommandLineOptions {\n db?: string | undefined;\n query?: string | undefined;\n name?: string | undefined;\n maxResults?: string | undefined;\n sort?: string | undefined;\n dryRun?: boolean | undefined;\n countOnly?: boolean | undefined;\n preview?: boolean | undefined;\n noResume?: boolean | undefined;\n strict?: boolean | undefined;\n}\n\nexport interface TranslationResult {\n provider: string;\n query: string;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\n/**\n * Options for enhanced dry-run output.\n */\nexport interface DryRunOutputOptions {\n config?: Config;\n providers?: ProviderName[];\n skipConnectionTest?: boolean | undefined;\n}\n\nexport function parseSearchOptions(\n queryFile: string | undefined,\n options: CommandLineOptions\n): SearchCommandOptions {\n const result: SearchCommandOptions = {};\n\n if (queryFile) {\n result.queryFile = queryFile;\n }\n\n if (options.query) {\n result.directQuery = options.query;\n }\n\n if (options.db) {\n result.providers = parseProviderNames(options.db);\n }\n\n if (options.name) {\n result.sessionName = options.name;\n }\n\n if (options.maxResults) {\n result.maxResults = parseInt(options.maxResults, 10);\n }\n\n if (options.dryRun) {\n result.dryRun = true;\n }\n\n if (options.countOnly) {\n result.countOnly = true;\n }\n\n if (options.preview) {\n result.preview = true;\n }\n\n if (options.noResume) {\n result.noResume = true;\n }\n\n if (options.strict) {\n result.strict = true;\n }\n\n if (options.sort === 'relevance' || options.sort === 'date') {\n result.sort = options.sort;\n }\n\n return result;\n}\n\nexport function validateSearchInput(options: SearchCommandOptions): ValidationResult {\n if (!options.queryFile && !options.directQuery) {\n return {\n valid: false,\n error: 'Either a query file or --query option is required',\n };\n }\n\n if (options.directQuery && (!options.providers || options.providers.length === 0)) {\n return {\n valid: false,\n error: 'Direct query (--query) requires --db option to specify the provider',\n };\n }\n\n if (options.directQuery && options.providers && options.providers.length > 1) {\n return {\n valid: false,\n error: 'Direct query (--query) can only be used with a single provider (--db)',\n };\n }\n\n if (options.preview && options.countOnly) {\n return {\n valid: false,\n error: '--preview and --count-only cannot be used together',\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Test provider connections and return results.\n */\nexport async function testProviderConnections(\n providers: ProviderName[],\n config: Config\n): Promise<Record<string, ConnectionTestResult>> {\n const results: Record<string, ConnectionTestResult> = {};\n await Promise.all(\n providers.map(async (name) => {\n const provider = createProviderInstance(name, config);\n if (!provider) {\n results[name] = { ok: false, error: 'Provider configuration incomplete' };\n return;\n }\n results[name] = await provider.testConnection();\n })\n );\n return results;\n}\n\n/**\n * Format provider readiness summary for dry-run output.\n */\nexport function formatProviderReadiness(\n providers: ProviderName[],\n config: Config,\n connectionResults?: Record<string, ConnectionTestResult>\n): string {\n const lines: string[] = [];\n lines.push('Provider readiness:');\n for (const provider of providers) {\n const providerConfig = config.providers[provider];\n const connResult = connectionResults?.[provider];\n const status = getProviderStatus(provider, providerConfig, connResult);\n const mark = status.ready ? '✓' : '✗';\n lines.push(` ${mark} ${provider.padEnd(10)}${status.message}`);\n }\n return lines.join('\\n');\n}\n\nfunction getProviderStatus(\n provider: ProviderName,\n providerConfig: { email?: string; api_key?: string },\n connectionResult?: ConnectionTestResult\n): { ready: boolean; message: string } {\n switch (provider) {\n case 'pubmed': {\n if (connectionResult && !connectionResult.ok) {\n return { ready: false, message: `not ready (${connectionResult.error})` };\n }\n if (!providerConfig.email) {\n return { ready: true, message: connectionResult ? 'ready (verified, email: not configured (recommended))' : 'ready (email: not configured (recommended))' };\n }\n return { ready: true, message: connectionResult ? 'ready (verified, email: configured)' : 'ready (email: configured)' };\n }\n case 'scopus': {\n if (!providerConfig.api_key) {\n return { ready: false, message: 'missing api_key (required)' };\n }\n if (connectionResult && !connectionResult.ok) {\n return { ready: false, message: `not ready (${connectionResult.error})` };\n }\n return { ready: true, message: connectionResult ? 'ready (verified)' : 'ready' };\n }\n case 'eric':\n case 'arxiv': {\n if (connectionResult && !connectionResult.ok) {\n return { ready: false, message: `not ready (${connectionResult.error})` };\n }\n return { ready: true, message: connectionResult ? 'ready (verified)' : 'ready' };\n }\n default:\n return { ready: true, message: 'ready' };\n }\n}\n\n/**\n * Format query diagnostics warnings for dry-run output.\n */\nexport function formatQueryDiagnostics(translations: TranslationResult[]): string {\n const warnings: string[] = [];\n for (const t of translations) {\n if (t.provider === 'pubmed') {\n if (/\\bNOT\\b/.test(t.query)) {\n warnings.push(' ⚠ pubmed: query uses NOT operator (ensure correct syntax for exclusions)');\n }\n if (/\\*\\[(?:mh|mesh)\\]/i.test(t.query)) {\n warnings.push(' ⚠ pubmed: wildcard in MeSH term — PubMed does not support wildcards in MeSH fields');\n }\n }\n }\n if (warnings.length === 0) {\n return '';\n }\n const lines: string[] = [];\n lines.push('Diagnostics:');\n lines.push(...warnings);\n return lines.join('\\n');\n}\n\n/**\n * Result of a count-only query for a single provider.\n */\nexport interface CountResult {\n provider: string;\n count: number;\n error?: string;\n}\n\n\n/**\n * Preview result for a single provider\n */\nexport interface PreviewResult {\n provider: string;\n count: number;\n titles: string[];\n error?: string;\n}\n\n/**\n * Format count-only output for display.\n */\nexport function formatCountOnlyOutput(\n counts: CountResult[],\n queryLabel?: string\n): string {\n const label = queryLabel ?? 'direct-query';\n const lines: string[] = [];\n\n lines.push(`Query: ${label} (count only)`);\n lines.push('');\n\n // Find the max provider name length for alignment (including colon)\n const maxNameLen = Math.max(...counts.map((c) => c.provider.length + 1), 6);\n\n // Calculate total (excluding errors)\n let total = 0;\n\n for (const c of counts) {\n if (c.error) {\n lines.push(` ${(c.provider + ':').padEnd(maxNameLen)} error: ${c.error}`);\n } else {\n const countStr = String(c.count).padStart(6);\n lines.push(` ${(c.provider + ':').padEnd(maxNameLen)} ${countStr} hits`);\n total += c.count;\n }\n }\n\n // Separator and total\n const separatorLen = maxNameLen + 14;\n lines.push(` ${'─'.repeat(separatorLen)}`);\n const totalStr = String(total).padStart(6);\n lines.push(` ${('total:').padEnd(maxNameLen)} ${totalStr} hits (before deduplication)`);\n\n return lines.join('\\n');\n}\n\n\n/**\n * Format preview output showing counts and sample titles.\n */\nexport function formatPreviewOutput(\n results: PreviewResult[],\n queryLabel?: string\n): string {\n const label = queryLabel ?? 'direct-query';\n const lines: string[] = [];\n\n lines.push(`Query: ${label} (preview)`);\n lines.push('');\n\n // Find the max provider name length for alignment (including colon)\n const maxNameLen = Math.max(...results.map((r) => r.provider.length + 1), 6);\n\n // Calculate total (excluding errors)\n let total = 0;\n\n for (const r of results) {\n if (r.error) {\n lines.push(` ${(r.provider + ':').padEnd(maxNameLen)} error: ${r.error}`);\n } else {\n const countStr = String(r.count).padStart(6);\n lines.push(` ${(r.provider + ':').padEnd(maxNameLen)} ${countStr} hits`);\n total += r.count;\n\n // Show sample titles\n if (r.titles.length > 0) {\n for (const title of r.titles) {\n lines.push(` • ${title}`);\n }\n }\n }\n lines.push('');\n }\n\n // Separator and total\n const separatorLen = maxNameLen + 14;\n lines.push(` ${'─'.repeat(separatorLen)}`);\n const totalStr = String(total).padStart(6);\n lines.push(` ${('total:').padEnd(maxNameLen)} ${totalStr} hits (before deduplication)`);\n\n return lines.join('\\n');\n}\n\nexport async function formatDryRunOutput(\n translations: TranslationResult[],\n options?: DryRunOutputOptions\n): Promise<string> {\n if (translations.length === 0) {\n return 'No translations available.';\n }\n const sections: string[] = [];\n if (options?.config && options?.providers) {\n let connectionResults: Record<string, ConnectionTestResult> | undefined;\n if (!options.skipConnectionTest) {\n connectionResults = await testProviderConnections(options.providers, options.config);\n }\n sections.push(formatProviderReadiness(options.providers, options.config, connectionResults));\n sections.push('');\n }\n const queryLines: string[] = [];\n queryLines.push('Translated queries:');\n queryLines.push('');\n for (const t of translations) {\n queryLines.push(`[${t.provider}]`);\n queryLines.push(t.query);\n queryLines.push('');\n }\n sections.push(queryLines.join('\\n'));\n const diagnostics = formatQueryDiagnostics(translations);\n if (diagnostics) {\n sections.push(diagnostics);\n sections.push('');\n }\n return sections.join('\\n');\n}\n\n\n/**\n * Format warning for short keywords that may cause noisy results.\n */\nexport function formatShortKeywordWarning(shortKeywords: string[]): string {\n if (shortKeywords.length === 0) {\n return '';\n }\n\n const keywordList = shortKeywords.join(', ');\n return `⚠ Query contains short keywords: ${keywordList}\n Short terms may match unrelated acronyms. Consider:\n - Adding full phrases (e.g., \"Objective Structured Clinical Examination\")\n - Using exclude terms to filter false matches`;\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8LpC;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAkjFvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAyM5C;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAiuFvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
package/dist/cli/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { config } from "dotenv";
3
- import { Command } from "commander";
3
+ import { Command, Option } from "commander";
4
4
  import { VERSION } from "../version.js";
5
5
  import { init } from "./commands/init.js";
6
6
  import { EXIT_CODES } from "./exit-codes.js";
@@ -30,6 +30,7 @@ import { filterByQuery } from "./commands/query-filter.js";
30
30
  import { loadNotes, formatAllSessionNotes, formatNotesList, addNote, addAssessment } from "./commands/notes.js";
31
31
  import { computeDiff, computeQueryDiff, formatDiffJson, formatDiff } from "./commands/diff.js";
32
32
  import { validateMergeSources, mergeArticles, formatMergeJson, formatMergeOutput, createMergedSession } from "./commands/merge.js";
33
+ import { parseRelatedOptions, validateRelatedInput, resolveSeeds, createRelatedSession, formatRelatedOutput } from "./commands/related.js";
33
34
  import { executeReviewInit } from "./commands/review/init.js";
34
35
  import { executeReviewStatus, formatStatusOutput } from "./commands/review/status.js";
35
36
  import { executeReviewList, formatListOutput } from "./commands/review/list.js";
@@ -41,7 +42,7 @@ import { executeReviewFinalize, formatFinalizeOutput } from "./commands/review/f
41
42
  import "zod";
42
43
  import "./commands/review/schema.js";
43
44
  import { registerFulltextCommands } from "./commands/fulltext/index.js";
44
- import { parseRegisterOptions, validateRegisterInput, hasReviewFile, getReviewSummary, formatNoIncludedArticlesError, formatPendingWarning, confirmPrompt, getIncludedArticles, formatReviewRequiredMessage, formatIgnoringReviewsNote, formatDryRunOutput as formatDryRunOutput$1, formatRegistrationSummary } from "./commands/register.js";
45
+ import { parseRegisterOptions, validateRegisterInput, hasReviewFile, getReviewSummary, formatNoIncludedArticlesError, formatPendingWarning, confirmPrompt, getIncludedArticles, formatReviewRequiredMessage, formatIgnoringReviewsNote, formatDryRunOutput as formatDryRunOutput$1, formatRegistrationSummary, formatLibraryPath, formatDefaultLibraryHint } from "./commands/register.js";
45
46
  import { formatSuggestion } from "./suggestions/index.js";
46
47
  import { getSuggestion } from "./suggestions/rules.js";
47
48
  import { readLogEntries, computeQueryHash, appendLogEntry, buildPreviewLogEntry, buildCountLogEntry } from "./commands/query/iteration-log.js";
@@ -59,6 +60,7 @@ import { getSessionsDir } from "./utils/sessions-dir.js";
59
60
  import { expandPath } from "../utils/path.js";
60
61
  import { loadSessionArticles, loadSessionQuery } from "./commands/session-utils.js";
61
62
  import { parseIdentifierFile, checkCoverage, formatCheckResultJson, formatCheckResult } from "./commands/check.js";
63
+ import { PubMedClient } from "../providers/pubmed/client.js";
62
64
  config({ quiet: true });
63
65
  function createProgram() {
64
66
  const program = new Command();
@@ -446,7 +448,7 @@ Examples:
446
448
  process.exitCode = EXIT_CODES.SESSION_ERROR;
447
449
  }
448
450
  });
449
- program.command("search").description("Execute search across databases").argument("[query-file]", "path to query YAML file").option("--db <providers>", "target specific database(s), comma-separated").option("--query <string>", "direct query in database-native syntax (advanced; requires --db; prefer YAML files)").option("--name <string>", "session name").option("--max-results <n>", "limit results per database").option("--dry-run", "show translated queries without executing").option("--count-only", "get hit counts without downloading results").option("--preview", "get hit counts and first 5 titles without creating session").option("--skip-connection-test", "skip API connection test during dry-run").option("--no-resume", "start fresh even if session exists").option("--strict", "require all targeted databases to succeed (exit non-zero on partial failure)").addHelpText("after", `
451
+ program.command("search").description("Execute search across databases").argument("[query-file]", "path to query YAML file").option("--db <providers>", "target specific database(s), comma-separated").option("--query <string>", "direct query in database-native syntax (advanced; requires --db; prefer YAML files)").option("--name <string>", "session name").option("--max-results <n>", "limit results per database").addOption(new Option("--sort <method>", "sort results by relevance or date").choices(["relevance", "date"])).option("--dry-run", "show translated queries without executing").option("--count-only", "get hit counts without downloading results").option("--preview", "get hit counts and first 5 titles without creating session").option("--skip-connection-test", "skip API connection test during dry-run").option("--no-resume", "start fresh even if session exists").option("--strict", "require all targeted databases to succeed (exit non-zero on partial failure)").addHelpText("after", `
450
452
  Workflow position:
451
453
  query validate → [this command: search] → results / summary / diff
452
454
 
@@ -471,6 +473,7 @@ Query features (use "query init" to see full template):
471
473
  query: options?.query,
472
474
  name: options?.name,
473
475
  maxResults: options?.maxResults,
476
+ sort: options?.sort,
474
477
  dryRun: options?.dryRun,
475
478
  countOnly: options?.countOnly,
476
479
  preview: options?.preview,
@@ -662,6 +665,11 @@ Search completed. Session: ${result.sessionId}`);
662
665
  } else {
663
666
  console.log(` ${provider}: ${stats.retrieved} results`);
664
667
  }
668
+ if (stats.warnings && stats.warnings.length > 0) {
669
+ for (const w of stats.warnings) {
670
+ console.warn(` ⚠ ${provider}: ${w}`);
671
+ }
672
+ }
665
673
  }
666
674
  }
667
675
  if (result.sessionStatus === "partial" && result.results) {
@@ -1285,6 +1293,119 @@ Input file format (one identifier per line):
1285
1293
  }
1286
1294
  }
1287
1295
  );
1296
+ program.command("related").description("Find related articles from seed PMIDs using PubMed ELink").argument("[pmids...]", "seed PMIDs").option("-n, --name <string>", "session name").option("-m, --max-results <number>", "max related articles to retrieve", "20").option("-s, --from-session <id>", "load seed PMIDs from existing session").option("--pmid <pmids...>", "seed PMIDs (alternative to positional args)").option("-t, --term <filter>", 'additional PubMed filter (e.g., "review[filter]")').addHelpText("after", `
1297
+ Examples:
1298
+ $ search-hub related 12345678 23456789 # Find related articles
1299
+ $ search-hub related 12345678 --name my-related # Custom session name
1300
+ $ search-hub related 12345678 -m 50 # Get more results
1301
+ $ search-hub related --from-session SESSION --pmid 12345678
1302
+ $ search-hub related 12345678 -t "review[filter]" # Filter by review type`).action(
1303
+ async (pmidArgs, options) => {
1304
+ const globalOpts = program.opts();
1305
+ try {
1306
+ const parsedOptions = parseRelatedOptions(pmidArgs, options ?? {});
1307
+ const validation = validateRelatedInput(parsedOptions);
1308
+ if (!validation.valid) {
1309
+ if (!globalOpts.quiet) {
1310
+ console.error(`Error: ${validation.error}`);
1311
+ }
1312
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
1313
+ return;
1314
+ }
1315
+ const sessionsDir = await getSessionsDir(globalOpts);
1316
+ let seedPmids;
1317
+ try {
1318
+ seedPmids = await resolveSeeds(parsedOptions, sessionsDir);
1319
+ } catch (error) {
1320
+ if (!globalOpts.quiet) {
1321
+ console.error(
1322
+ `Error: ${error instanceof Error ? error.message : "Failed to resolve seeds"}`
1323
+ );
1324
+ }
1325
+ process.exitCode = EXIT_CODES.SESSION_ERROR;
1326
+ return;
1327
+ }
1328
+ if (seedPmids.length === 0) {
1329
+ if (!globalOpts.quiet) {
1330
+ console.error("Error: No PMIDs found to use as seeds.");
1331
+ }
1332
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
1333
+ return;
1334
+ }
1335
+ const config2 = await loadConfig(
1336
+ globalOpts.config ? { explicitConfigPath: globalOpts.config } : {}
1337
+ );
1338
+ const providerConfig = config2.providers.pubmed;
1339
+ const pubmedConfig = {
1340
+ email: providerConfig.email ?? "search-hub@example.com",
1341
+ rateLimit: providerConfig.rate_limit,
1342
+ timeout: providerConfig.timeout,
1343
+ retries: providerConfig.retries
1344
+ };
1345
+ if (providerConfig.api_key) {
1346
+ pubmedConfig.apiKey = providerConfig.api_key;
1347
+ }
1348
+ const rateLimiter = new RateLimiter({
1349
+ tokensPerSecond: pubmedConfig.rateLimit ?? (pubmedConfig.apiKey ? 10 : 3)
1350
+ });
1351
+ const client = new PubMedClient(pubmedConfig, rateLimiter);
1352
+ if (!globalOpts.quiet) {
1353
+ console.log(`Finding related articles for ${seedPmids.length} seed PMIDs...`);
1354
+ }
1355
+ const relatedArticles = await client.findRelatedMerged({
1356
+ ids: seedPmids,
1357
+ maxResults: parsedOptions.maxResults,
1358
+ ...parsedOptions.term && { term: parsedOptions.term }
1359
+ });
1360
+ const totalRelated = relatedArticles.length;
1361
+ if (totalRelated === 0) {
1362
+ if (!globalOpts.quiet) {
1363
+ console.log("No related articles found.");
1364
+ }
1365
+ process.exitCode = EXIT_CODES.SUCCESS;
1366
+ return;
1367
+ }
1368
+ const relatedPmids = relatedArticles.map((a) => a.id);
1369
+ const articles = await client.fetch(relatedPmids);
1370
+ const sessionName = parsedOptions.name ?? `related-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
1371
+ const sessionFile = await createRelatedSession({
1372
+ name: sessionName,
1373
+ seeds: {
1374
+ ids: seedPmids,
1375
+ ...parsedOptions.fromSession && { sourceSession: parsedOptions.fromSession }
1376
+ },
1377
+ articles,
1378
+ sessionsDir
1379
+ });
1380
+ if (!globalOpts.quiet) {
1381
+ console.log(formatRelatedOutput({
1382
+ sessionId: sessionFile.id,
1383
+ seedCount: seedPmids.length,
1384
+ totalRelated,
1385
+ retrievedCount: articles.length,
1386
+ articles
1387
+ }));
1388
+ const suggestion = getSuggestion({
1389
+ command: "related",
1390
+ sessionId: sessionFile.id
1391
+ });
1392
+ const suggestionText = formatSuggestion(suggestion);
1393
+ if (suggestionText) {
1394
+ console.log(suggestionText);
1395
+ }
1396
+ }
1397
+ process.exitCode = EXIT_CODES.SUCCESS;
1398
+ } catch (error) {
1399
+ if (!globalOpts.quiet) {
1400
+ console.error(
1401
+ "Error:",
1402
+ error instanceof Error ? error.message : error
1403
+ );
1404
+ }
1405
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
1406
+ }
1407
+ }
1408
+ );
1288
1409
  program.command("merge").description("Merge results from multiple search sessions").argument("<session-ids...>", "two or more session IDs to merge").option("--name <string>", "name for merged session").option("--dry-run", "show what would be merged without creating session").option("--json", "output as JSON").addHelpText("after", `
1289
1410
  Examples:
1290
1411
  $ search-hub merge session-v4 session-v9 # Merge two sessions
@@ -1567,7 +1688,10 @@ Failed to install reference-manager: ${installError instanceof Error ? installEr
1567
1688
  }
1568
1689
  }
1569
1690
  console.log(`
1570
- Results saved to: ${join(sessionDir, "registration.json")}`);
1691
+ ${formatLibraryPath(sessionDir)}`);
1692
+ console.log(`Results saved to: ${join(sessionDir, "registration.json")}`);
1693
+ console.log(`
1694
+ ${formatDefaultLibraryHint(sessionDir)}`);
1571
1695
  const suggestion = formatSuggestion(getSuggestion({
1572
1696
  command: "register",
1573
1697
  sessionId,
@@ -1595,12 +1719,16 @@ Examples:
1595
1719
  $ search-hub review extract --session SESSION_ID --name title-screening # Extract for review
1596
1720
  $ search-hub review merge --session SESSION_ID --name title-screening # Merge reviews
1597
1721
  $ search-hub review export --session SESSION_ID --only included -o included.yaml`);
1598
- reviewCommand.command("init").description("Generate reviews.yaml from deduplicated search results").requiredOption("--session <id>", "session ID").option("-f, --force", "overwrite existing reviews.yaml", false).action(async (options) => {
1722
+ reviewCommand.command("init").description("Generate reviews.yaml from deduplicated search results").requiredOption("--session <id>", "session ID").option("--mode <mode>", "review mode: screening (exclusion-based) or picking (inclusion-based)").option("-f, --force", "overwrite existing reviews.yaml", false).action(async (options) => {
1599
1723
  const globalOpts = program.opts();
1600
1724
  try {
1725
+ if (options.mode && options.mode !== "screening" && options.mode !== "picking") {
1726
+ throw new Error(`Invalid mode: "${options.mode}". Must be "screening" or "picking".`);
1727
+ }
1601
1728
  const sessionsDir = await getSessionsDir(globalOpts);
1602
1729
  const initOptions = {
1603
1730
  sessionId: options.session,
1731
+ ...options.mode && { mode: options.mode },
1604
1732
  ...options.force && { force: options.force }
1605
1733
  };
1606
1734
  const result = await executeReviewInit(initOptions, sessionsDir);