@ncukondo/search-hub 0.18.0 → 0.20.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.
Files changed (83) hide show
  1. package/dist/cli/commands/register.d.ts +1 -1
  2. package/dist/cli/commands/register.js.map +1 -1
  3. package/dist/cli/commands/related.d.ts +66 -0
  4. package/dist/cli/commands/related.d.ts.map +1 -0
  5. package/dist/cli/commands/related.js +161 -0
  6. package/dist/cli/commands/related.js.map +1 -0
  7. package/dist/cli/commands/review/extract.d.ts.map +1 -1
  8. package/dist/cli/commands/review/extract.js +15 -5
  9. package/dist/cli/commands/review/extract.js.map +1 -1
  10. package/dist/cli/commands/review/finalize.js +6 -6
  11. package/dist/cli/commands/review/finalize.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/list.d.ts +1 -1
  17. package/dist/cli/commands/review/list.d.ts.map +1 -1
  18. package/dist/cli/commands/review/list.js.map +1 -1
  19. package/dist/cli/commands/review/next-steps.d.ts +4 -1
  20. package/dist/cli/commands/review/next-steps.d.ts.map +1 -1
  21. package/dist/cli/commands/review/next-steps.js +53 -19
  22. package/dist/cli/commands/review/next-steps.js.map +1 -1
  23. package/dist/cli/commands/review/schema.d.ts +8 -0
  24. package/dist/cli/commands/review/schema.d.ts.map +1 -1
  25. package/dist/cli/commands/review/schema.js +3 -0
  26. package/dist/cli/commands/review/schema.js.map +1 -1
  27. package/dist/cli/commands/review/status.d.ts +5 -3
  28. package/dist/cli/commands/review/status.d.ts.map +1 -1
  29. package/dist/cli/commands/review/status.js +16 -14
  30. package/dist/cli/commands/review/status.js.map +1 -1
  31. package/dist/cli/commands/review/types.d.ts +7 -6
  32. package/dist/cli/commands/review/types.d.ts.map +1 -1
  33. package/dist/cli/commands/review/types.js +6 -9
  34. package/dist/cli/commands/review/types.js.map +1 -1
  35. package/dist/cli/commands/search-executor.d.ts.map +1 -1
  36. package/dist/cli/commands/search-executor.js +3 -2
  37. package/dist/cli/commands/search-executor.js.map +1 -1
  38. package/dist/cli/commands/search.d.ts +2 -0
  39. package/dist/cli/commands/search.d.ts.map +1 -1
  40. package/dist/cli/commands/search.js +3 -0
  41. package/dist/cli/commands/search.js.map +1 -1
  42. package/dist/cli/index.d.ts.map +1 -1
  43. package/dist/cli/index.js +131 -6
  44. package/dist/cli/index.js.map +1 -1
  45. package/dist/cli/suggestions/rules.d.ts.map +1 -1
  46. package/dist/cli/suggestions/rules.js +19 -3
  47. package/dist/cli/suggestions/rules.js.map +1 -1
  48. package/dist/providers/arxiv/provider.d.ts.map +1 -1
  49. package/dist/providers/arxiv/provider.js +7 -4
  50. package/dist/providers/arxiv/provider.js.map +1 -1
  51. package/dist/providers/base/types.d.ts +8 -0
  52. package/dist/providers/base/types.d.ts.map +1 -1
  53. package/dist/providers/base/types.js.map +1 -1
  54. package/dist/providers/eric/provider.d.ts +3 -0
  55. package/dist/providers/eric/provider.d.ts.map +1 -1
  56. package/dist/providers/eric/provider.js +11 -0
  57. package/dist/providers/eric/provider.js.map +1 -1
  58. package/dist/providers/pubmed/client.d.ts +15 -1
  59. package/dist/providers/pubmed/client.d.ts.map +1 -1
  60. package/dist/providers/pubmed/client.js +64 -1
  61. package/dist/providers/pubmed/client.js.map +1 -1
  62. package/dist/providers/pubmed/index.d.ts +2 -2
  63. package/dist/providers/pubmed/index.d.ts.map +1 -1
  64. package/dist/providers/pubmed/parser.d.ts +8 -1
  65. package/dist/providers/pubmed/parser.d.ts.map +1 -1
  66. package/dist/providers/pubmed/parser.js +23 -1
  67. package/dist/providers/pubmed/parser.js.map +1 -1
  68. package/dist/providers/pubmed/provider.d.ts.map +1 -1
  69. package/dist/providers/pubmed/provider.js +8 -2
  70. package/dist/providers/pubmed/provider.js.map +1 -1
  71. package/dist/providers/pubmed/types.d.ts +29 -0
  72. package/dist/providers/pubmed/types.d.ts.map +1 -1
  73. package/dist/providers/scopus/client.d.ts +2 -0
  74. package/dist/providers/scopus/client.d.ts.map +1 -1
  75. package/dist/providers/scopus/client.js +3 -0
  76. package/dist/providers/scopus/client.js.map +1 -1
  77. package/dist/providers/scopus/provider.d.ts.map +1 -1
  78. package/dist/providers/scopus/provider.js +7 -1
  79. package/dist/providers/scopus/provider.js.map +1 -1
  80. package/dist/session/types.d.ts +13 -1
  81. package/dist/session/types.d.ts.map +1 -1
  82. package/dist/session/types.js.map +1 -1
  83. package/package.json +1 -1
@@ -255,7 +255,8 @@ async function executeSearch(options, sessionsDir, config, showProgress = true)
255
255
  let totalHits = 0;
256
256
  progress?.update(providerName, 0, 0, "in_progress");
257
257
  const searchOptions = {
258
- maxResults: options.maxResults ?? config.providers[providerName].max_results
258
+ maxResults: options.maxResults ?? config.providers[providerName].max_results,
259
+ ...options.sort && { sort: options.sort }
259
260
  };
260
261
  for await (const article of provider.search(translatedQuery, searchOptions)) {
261
262
  retrievedCount++;
@@ -289,7 +290,7 @@ async function executeSearch(options, sessionsDir, config, showProgress = true)
289
290
  },
290
291
  sessionsDir
291
292
  );
292
- const providerWarnings = "getWarnings" in provider && typeof provider.getWarnings === "function" ? provider.getWarnings() : void 0;
293
+ const providerWarnings = provider.getWarnings?.();
293
294
  results[providerName] = { hits: totalHits, retrieved: retrievedCount, ...providerWarnings && providerWarnings.length > 0 && { warnings: providerWarnings } };
294
295
  } catch (error) {
295
296
  const errorMessage = error instanceof Error ? error.message : isProviderError(error) ? error.message : String(error);
@@ -1 +1 @@
1
- {"version":3,"file":"search-executor.js","sources":["../../../src/cli/commands/search-executor.ts"],"sourcesContent":["/**\n * Search executor for CLI search command.\n *\n * Handles the actual execution of searches across multiple providers,\n * including session creation, progress display, and result storage.\n */\nimport { readFile, writeFile, appendFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { createHash } from 'node:crypto';\nimport type { SearchCommandOptions, CountResult, PreviewResult } from './search.js';\nimport type { Config } from '../../config/index.js';\nimport type {\n Article,\n Provider,\n ProviderName,\n TranslatedQuery,\n} from '../../providers/base/types.js';\nimport { isProviderError } from '../../providers/base/types.js';\nimport type { QueryAST } from '../../query/types.js';\nimport { parseQueryString } from '../../query/index.js';\nimport { resolveForProvider } from '../../query/resolver.js';\nimport {\n createSession,\n updateDatabaseStatus,\n updateSessionStatus,\n} from '../../session/manager.js';\nimport { MultiProviderProgress } from '../utils/progress.js';\nimport { PubMedProvider } from '../../providers/pubmed/provider.js';\nimport type { PubMedConfig } from '../../providers/pubmed/types.js';\nimport { ERICProvider } from '../../providers/eric/provider.js';\nimport { ArxivProvider } from '../../providers/arxiv/provider.js';\nimport { ScopusProvider } from '../../providers/scopus/provider.js';\nimport type { ScopusConfig } from '../../providers/scopus/types.js';\nimport { translateQuery as translatePubmed } from '../../providers/pubmed/translator.js';\nimport { translateQuery as translateEric } from '../../providers/eric/translator.js';\nimport { translateQuery as translateArxiv } from '../../providers/arxiv/translator.js';\nimport { translateQuery as translateScopus } from '../../providers/scopus/translator.js';\nimport { stringify as stringifyYaml } from 'yaml';\nimport { registerArticles, saveRegistrationRecord } from '../../integration/register.js';\nimport { buildFailureErrorMessage, buildPartialErrorMessage } from './search-utils.js';\nimport { getConfigDir } from '../../config/paths.js';\nimport type { RegistrationRecord } from '../../integration/types.js';\nimport { checkRefAvailable } from '../../integration/ref-cli.js';\nimport { convertResultsToYaml, loadResults } from '../../session/results-io.js';\n\n/**\n * Result of a search execution.\n */\nexport interface SearchExecutionResult {\n success: boolean;\n sessionId?: string;\n results?: Record<string, { hits: number; retrieved: number; error?: string; warnings?: string[] }>;\n error?: string;\n autoRegisterResult?: RegistrationRecord;\n sessionStatus: 'completed' | 'partial' | 'failed';\n}\n\n/**\n * Available providers that are implemented.\n */\nconst IMPLEMENTED_PROVIDERS: ProviderName[] = ['pubmed', 'eric', 'arxiv', 'scopus'];\n\n/**\n * Check if a provider has the required configuration (e.g., API keys).\n * Providers that require no special configuration always return true.\n */\nexport function isProviderConfigured(name: ProviderName, config: Config): boolean {\n switch (name) {\n case 'scopus':\n return !!config.providers.scopus.api_key;\n default:\n return true; // pubmed, eric, arxiv require no API key\n }\n}\n\n/**\n * Create a provider instance for the given provider name.\n */\nexport function createProviderInstance(\n name: ProviderName,\n config: Config\n): Provider | null {\n const providerConfig = config.providers[name];\n\n switch (name) {\n case 'pubmed': {\n if (!providerConfig.email) {\n const configPath = getConfigDir();\n console.warn(\n `Warning: No email configured for PubMed.\\n` +\n ` → Edit ${configPath}/config.toml and set providers.pubmed.email\\n` +\n ` → Or run: search-hub config providers.pubmed.email \"your@email.com\"`\n );\n }\n const pubmedOpts: PubMedConfig = {\n email: providerConfig.email ?? 'search-hub@example.com',\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n };\n if (providerConfig.api_key) {\n pubmedOpts.apiKey = providerConfig.api_key;\n }\n return new PubMedProvider(pubmedOpts);\n }\n case 'eric':\n return new ERICProvider({\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n });\n case 'arxiv':\n return new ArxivProvider({\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n });\n case 'scopus': {\n if (!providerConfig.api_key) {\n console.warn(\n `Warning: Scopus requires an API key. Set providers.scopus.api_key in config.\\n` +\n ` → Get an API key at https://dev.elsevier.com/\\n` +\n ` → Run: search-hub config providers.scopus.api_key \"your-key\"`\n );\n return null;\n }\n const scopusOpts: ScopusConfig = {\n apiKey: providerConfig.api_key,\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n };\n if (providerConfig.inst_token) {\n scopusOpts.instToken = providerConfig.inst_token;\n }\n return new ScopusProvider(scopusOpts);\n }\n default:\n throw new Error(`Provider '${name}' is not implemented`);\n }\n}\n\n/**\n * Translate a query AST for a specific provider.\n * Resolves provider-specific blocks/filters before translation.\n */\nfunction translateQueryForProvider(\n ast: QueryAST,\n provider: ProviderName\n): TranslatedQuery {\n const resolved = resolveForProvider(ast, provider);\n switch (provider) {\n case 'pubmed':\n return translatePubmed(resolved);\n case 'eric':\n return translateEric(resolved);\n case 'arxiv':\n return translateArxiv(resolved);\n case 'scopus':\n return translateScopus(resolved);\n default:\n throw new Error(`No translator for provider '${provider}'`);\n }\n}\n\n/**\n * Get enabled providers from config, optionally filtered by user selection.\n */\nfunction getEnabledProviders(\n config: Config,\n requestedProviders?: ProviderName[]\n): ProviderName[] {\n const enabledInConfig = IMPLEMENTED_PROVIDERS.filter(\n (name) => config.providers[name].enabled\n );\n\n if (requestedProviders && requestedProviders.length > 0) {\n return requestedProviders.filter((p) => enabledInConfig.includes(p));\n }\n\n return enabledInConfig;\n}\n\n/**\n * Execute a search across multiple providers.\n */\nexport async function executeSearch(\n options: SearchCommandOptions,\n sessionsDir: string,\n config: Config,\n showProgress = true\n): Promise<SearchExecutionResult> {\n let ast: QueryAST | undefined;\n let queryContent: string;\n let queryFile: string;\n\n // Handle direct query mode\n if (options.directQuery && options.providers && options.providers.length === 1) {\n queryFile = 'direct-query';\n\n // For direct query, we create a minimal AST structure\n ast = {\n name: options.sessionName ?? 'direct-query',\n blocks: [\n {\n id: 'direct',\n field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n providers: {},\n };\n\n // Generate YAML safely using yaml library to handle special characters\n queryContent = stringifyYaml({\n name: ast.name,\n blocks: ast.blocks,\n filters: ast.filters,\n });\n } else if (options.queryFile) {\n // Parse query file\n try {\n queryContent = await readFile(options.queryFile, 'utf-8');\n ast = parseQueryString(queryContent);\n queryFile = options.queryFile;\n } catch (error) {\n return {\n success: false,\n sessionStatus: 'failed',\n error: `Failed to parse query file: ${error instanceof Error ? error.message : error}`,\n };\n }\n } else {\n return {\n success: false,\n sessionStatus: 'failed',\n error: 'Either queryFile or directQuery with provider is required',\n };\n }\n\n // Determine which providers to use\n let providers = getEnabledProviders(config, options.providers);\n\n // In default mode (no --db), skip unconfigured providers with a warning\n const isExplicitSelection = options.providers && options.providers.length > 0;\n if (!isExplicitSelection) {\n const skipped: ProviderName[] = [];\n providers = providers.filter((name) => {\n if (!isProviderConfigured(name, config)) {\n skipped.push(name);\n return false;\n }\n return true;\n });\n for (const name of skipped) {\n console.warn(\n `Skipping ${name}: API key not configured (use --db ${name} to force)`\n );\n }\n }\n\n if (providers.length === 0) {\n return {\n success: false,\n sessionStatus: 'failed',\n error: 'No providers enabled or selected',\n };\n }\n\n // Create query hash\n const queryHash = createHash('sha256').update(queryContent).digest('hex').slice(0, 8);\n\n // Create session\n let session;\n try {\n const sessionOpts: Parameters<typeof createSession>[0] = {\n name: options.sessionName ?? ast.name,\n queryFile,\n queryContent,\n queryHash,\n targets: providers,\n sessionsDir,\n };\n if (ast.description) {\n sessionOpts.description = ast.description;\n }\n session = await createSession(sessionOpts);\n } catch (error) {\n return {\n success: false,\n sessionStatus: 'failed',\n error: `Failed to create session: ${error instanceof Error ? error.message : error}`,\n };\n }\n\n const sessionId = session.id;\n const results: Record<string, { hits: number; retrieved: number; error?: string; warnings?: string[] }> = {};\n\n // Create progress display if enabled\n let progress: MultiProviderProgress | undefined;\n if (showProgress && process.stdout.isTTY) {\n progress = new MultiProviderProgress(providers);\n }\n\n // Execute search for each provider\n for (const providerName of providers) {\n try {\n // Create provider instance\n const provider = createProviderInstance(providerName, config);\n\n // Skip provider if it could not be created (e.g. missing configuration)\n if (provider === null) {\n const configError = `${providerName}: provider configuration incomplete. See warning above for details.`;\n results[providerName] = { hits: 0, retrieved: 0, error: configError };\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'failed',\n completedAt: new Date().toISOString(),\n error: {\n code: 'CONFIG_ERROR',\n message: configError,\n retryable: false,\n },\n },\n sessionsDir\n );\n continue;\n }\n\n // Translate query\n let translatedQuery: TranslatedQuery;\n if (options.directQuery && options.providers?.length === 1) {\n // For direct query, use the native query string directly\n translatedQuery = {\n native: options.directQuery,\n provider: providerName,\n };\n } else {\n translatedQuery = translateQueryForProvider(ast, providerName);\n }\n\n // Write translated query to session\n const queryPath = join(sessionsDir, sessionId, `${providerName}_query.txt`);\n await writeFile(queryPath, translatedQuery.native, 'utf-8');\n\n // Update database status to in_progress\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'in_progress',\n startedAt: new Date().toISOString(),\n },\n sessionsDir\n );\n\n // Prepare results file path\n const resultsPath = join(sessionsDir, sessionId, `${providerName}_results.jsonl`);\n\n // Execute search\n let retrievedCount = 0;\n let totalHits = 0;\n\n progress?.update(providerName, 0, 0, 'in_progress');\n\n const searchOptions = {\n maxResults: options.maxResults ?? config.providers[providerName].max_results,\n };\n\n for await (const article of provider.search(translatedQuery, searchOptions)) {\n retrievedCount++;\n\n // Write article to JSONL file\n await appendFile(resultsPath, JSON.stringify(article) + '\\n', 'utf-8');\n\n // Update progress (estimate total from first batch)\n if (totalHits === 0) {\n // Estimate total - this is provider-dependent, we'll use retrieved count as minimum\n totalHits = Math.max(retrievedCount * 10, 100);\n }\n progress?.update(providerName, retrievedCount, totalHits, 'in_progress');\n }\n\n // Update final totals\n totalHits = retrievedCount; // Use actual count as final total\n\n // Mark as completed\n progress?.complete(providerName);\n\n // Convert JSONL to YAML for human-readable view\n const yamlFilename = `${providerName}_results.yaml`;\n const yamlPath = join(sessionsDir, sessionId, yamlFilename);\n await convertResultsToYaml(resultsPath, yamlPath, {\n provider: providerName,\n queryName: ast.name,\n });\n\n // Update database status\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'completed',\n completedAt: new Date().toISOString(),\n totalHits,\n retrievedCount,\n files: {\n query: `${providerName}_query.txt`,\n results: `${providerName}_results.jsonl`,\n resultsYaml: yamlFilename,\n },\n },\n sessionsDir\n );\n\n // Collect warnings if provider supports them\n const providerWarnings = 'getWarnings' in provider && typeof (provider as any).getWarnings === 'function'\n ? (provider as any).getWarnings() as string[]\n : undefined;\n results[providerName] = { hits: totalHits, retrieved: retrievedCount, ...(providerWarnings && providerWarnings.length > 0 && { warnings: providerWarnings }) };\n } catch (error) {\n const errorMessage = error instanceof Error\n ? error.message\n : isProviderError(error)\n ? error.message\n : String(error);\n\n progress?.fail(providerName, errorMessage);\n\n // Update database status with error\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'failed',\n completedAt: new Date().toISOString(),\n error: {\n code: 'SEARCH_ERROR',\n message: errorMessage,\n retryable: true,\n },\n },\n sessionsDir\n );\n\n results[providerName] = { hits: 0, retrieved: 0, error: errorMessage };\n }\n }\n\n // Stop progress display\n progress?.stop();\n\n // Determine overall session status\n const anyFailed = providers.some((p) => {\n const r = results[p];\n return r && r.error !== undefined;\n });\n const anySucceeded = providers.some((p) => {\n const r = results[p];\n return r && r.retrieved > 0;\n });\n\n let sessionStatus: 'completed' | 'partial' | 'failed';\n if (!anyFailed) {\n sessionStatus = 'completed';\n } else if (anySucceeded) {\n sessionStatus = 'partial';\n } else {\n sessionStatus = 'failed';\n }\n\n // Update session status\n await updateSessionStatus(sessionId, sessionStatus, sessionsDir);\n\n if (sessionStatus === 'failed') {\n return {\n success: false,\n sessionId,\n sessionStatus,\n results,\n error: buildFailureErrorMessage(results),\n };\n }\n\n // In strict mode, partial success is treated as failure\n if (options.strict && sessionStatus === 'partial') {\n return {\n success: false,\n sessionId,\n sessionStatus,\n results,\n error: buildPartialErrorMessage(results),\n };\n }\n\n // Auto-register if enabled\n let autoRegisterResult: RegistrationRecord | undefined;\n if (\n config.integration.reference_manager.enabled &&\n config.integration.reference_manager.auto_register\n ) {\n const refAvailable = await checkRefAvailable();\n if (refAvailable) {\n // Load all articles from results files\n const allArticles = await loadArticlesFromSession(sessionsDir, sessionId, providers);\n\n if (allArticles.length > 0) {\n autoRegisterResult = await registerArticles(allArticles, {\n sessionId,\n sessionDir: join(sessionsDir, sessionId),\n withAbstracts: config.integration.reference_manager.with_abstracts,\n });\n\n // Save registration record\n await saveRegistrationRecord(join(sessionsDir, sessionId), autoRegisterResult);\n }\n }\n }\n\n const result: SearchExecutionResult = {\n success: true,\n sessionId,\n results,\n sessionStatus,\n };\n\n if (autoRegisterResult) {\n result.autoRegisterResult = autoRegisterResult;\n }\n\n return result;\n}\n\n/**\n * Execute count-only mode: get hit counts from each provider without downloading results.\n * No session is created.\n */\nexport async function executeCountOnly(\n options: SearchCommandOptions,\n config: Config\n): Promise<CountResult[]> {\n let ast: QueryAST | undefined;\n\n // Handle direct query mode\n if (options.directQuery && options.providers && options.providers.length === 1) {\n ast = {\n name: options.sessionName ?? 'direct-query',\n blocks: [\n {\n id: 'direct',\n field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n providers: {},\n };\n } else if (options.queryFile) {\n const queryContent = await readFile(options.queryFile, 'utf-8');\n ast = parseQueryString(queryContent);\n } else {\n return [];\n }\n\n // Determine which providers to use\n let providers = getEnabledProviders(config, options.providers);\n\n // In default mode (no --db), skip unconfigured providers\n const isExplicitSelection = options.providers && options.providers.length > 0;\n if (!isExplicitSelection) {\n providers = providers.filter((name) => isProviderConfigured(name, config));\n }\n\n if (providers.length === 0) {\n return [];\n }\n\n // Execute count for each provider concurrently\n const results: CountResult[] = await Promise.all(\n providers.map(async (providerName): Promise<CountResult> => {\n try {\n const provider = createProviderInstance(providerName, config);\n if (provider === null) {\n return { provider: providerName, count: 0, error: 'Provider configuration incomplete' };\n }\n\n // Translate query\n let translatedQuery: TranslatedQuery;\n if (options.directQuery && options.providers?.length === 1) {\n translatedQuery = {\n native: options.directQuery,\n provider: providerName,\n };\n } else {\n translatedQuery = translateQueryForProvider(ast!, providerName);\n }\n\n const count = await provider.count(translatedQuery);\n return { provider: providerName, count };\n } catch (error) {\n const errorMessage = error instanceof Error\n ? error.message\n : isProviderError(error)\n ? error.message\n : String(error);\n return { provider: providerName, count: 0, error: errorMessage };\n }\n })\n );\n\n return results;\n}\n\n\n/**\n * Execute preview mode: get counts and first few titles without creating a session.\n */\nexport async function executePreview(\n options: SearchCommandOptions,\n config: Config,\n maxTitles = 5\n): Promise<PreviewResult[]> {\n let ast: QueryAST | undefined;\n\n // Handle direct query mode\n if (options.directQuery && options.providers && options.providers.length === 1) {\n ast = {\n name: options.sessionName ?? 'direct-query',\n blocks: [\n {\n id: 'direct',\n field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n providers: {},\n };\n } else if (options.queryFile) {\n const queryContent = await readFile(options.queryFile, 'utf-8');\n ast = parseQueryString(queryContent);\n } else {\n return [];\n }\n\n // Determine which providers to use\n let providers = getEnabledProviders(config, options.providers);\n\n // In default mode (no --db), skip unconfigured providers\n const isExplicitSelection = options.providers && options.providers.length > 0;\n if (!isExplicitSelection) {\n providers = providers.filter((name) => isProviderConfigured(name, config));\n }\n\n if (providers.length === 0) {\n return [];\n }\n\n // Execute preview for each provider concurrently\n const results: PreviewResult[] = await Promise.all(\n providers.map(async (providerName): Promise<PreviewResult> => {\n try {\n const provider = createProviderInstance(providerName, config);\n if (provider === null) {\n return { provider: providerName, count: 0, titles: [], error: 'Provider configuration incomplete' };\n }\n\n // Translate query\n let translatedQuery: TranslatedQuery;\n if (options.directQuery && options.providers?.length === 1) {\n translatedQuery = {\n native: options.directQuery,\n provider: providerName,\n };\n } else {\n translatedQuery = translateQueryForProvider(ast!, providerName);\n }\n\n // Get count first\n const count = await provider.count(translatedQuery);\n\n // Collect first few articles for titles\n const titles: string[] = [];\n const searchOptions = { maxResults: maxTitles };\n\n for await (const article of provider.search(translatedQuery, searchOptions)) {\n titles.push(article.title);\n if (titles.length >= maxTitles) {\n break;\n }\n }\n\n return { provider: providerName, count, titles };\n } catch (error) {\n const errorMessage = error instanceof Error\n ? error.message\n : isProviderError(error)\n ? error.message\n : String(error);\n return { provider: providerName, count: 0, titles: [], error: errorMessage };\n }\n })\n );\n\n return results;\n}\n\n/**\n * Load all articles from a session's results files (YAML preferred, JSONL fallback).\n */\nasync function loadArticlesFromSession(\n sessionsDir: string,\n sessionId: string,\n providers: ProviderName[]\n): Promise<Article[]> {\n const articles: Article[] = [];\n const sessionDir = join(sessionsDir, sessionId);\n\n for (const provider of providers) {\n const providerArticles = await loadResults(sessionDir, provider);\n articles.push(...providerArticles);\n }\n\n return articles;\n}\n"],"names":["translatePubmed","translateEric","translateArxiv","translateScopus","stringifyYaml"],"mappings":"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"}
1
+ {"version":3,"file":"search-executor.js","sources":["../../../src/cli/commands/search-executor.ts"],"sourcesContent":["/**\n * Search executor for CLI search command.\n *\n * Handles the actual execution of searches across multiple providers,\n * including session creation, progress display, and result storage.\n */\nimport { readFile, writeFile, appendFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { createHash } from 'node:crypto';\nimport type { SearchCommandOptions, CountResult, PreviewResult } from './search.js';\nimport type { Config } from '../../config/index.js';\nimport type {\n Article,\n Provider,\n ProviderName,\n TranslatedQuery,\n} from '../../providers/base/types.js';\nimport { isProviderError } from '../../providers/base/types.js';\nimport type { QueryAST } from '../../query/types.js';\nimport { parseQueryString } from '../../query/index.js';\nimport { resolveForProvider } from '../../query/resolver.js';\nimport {\n createSession,\n updateDatabaseStatus,\n updateSessionStatus,\n} from '../../session/manager.js';\nimport { MultiProviderProgress } from '../utils/progress.js';\nimport { PubMedProvider } from '../../providers/pubmed/provider.js';\nimport type { PubMedConfig } from '../../providers/pubmed/types.js';\nimport { ERICProvider } from '../../providers/eric/provider.js';\nimport { ArxivProvider } from '../../providers/arxiv/provider.js';\nimport { ScopusProvider } from '../../providers/scopus/provider.js';\nimport type { ScopusConfig } from '../../providers/scopus/types.js';\nimport { translateQuery as translatePubmed } from '../../providers/pubmed/translator.js';\nimport { translateQuery as translateEric } from '../../providers/eric/translator.js';\nimport { translateQuery as translateArxiv } from '../../providers/arxiv/translator.js';\nimport { translateQuery as translateScopus } from '../../providers/scopus/translator.js';\nimport { stringify as stringifyYaml } from 'yaml';\nimport { registerArticles, saveRegistrationRecord } from '../../integration/register.js';\nimport { buildFailureErrorMessage, buildPartialErrorMessage } from './search-utils.js';\nimport { getConfigDir } from '../../config/paths.js';\nimport type { RegistrationRecord } from '../../integration/types.js';\nimport { checkRefAvailable } from '../../integration/ref-cli.js';\nimport { convertResultsToYaml, loadResults } from '../../session/results-io.js';\n\n/**\n * Result of a search execution.\n */\nexport interface SearchExecutionResult {\n success: boolean;\n sessionId?: string;\n results?: Record<string, { hits: number; retrieved: number; error?: string; warnings?: string[] }>;\n error?: string;\n autoRegisterResult?: RegistrationRecord;\n sessionStatus: 'completed' | 'partial' | 'failed';\n}\n\n/**\n * Available providers that are implemented.\n */\nconst IMPLEMENTED_PROVIDERS: ProviderName[] = ['pubmed', 'eric', 'arxiv', 'scopus'];\n\n/**\n * Check if a provider has the required configuration (e.g., API keys).\n * Providers that require no special configuration always return true.\n */\nexport function isProviderConfigured(name: ProviderName, config: Config): boolean {\n switch (name) {\n case 'scopus':\n return !!config.providers.scopus.api_key;\n default:\n return true; // pubmed, eric, arxiv require no API key\n }\n}\n\n/**\n * Create a provider instance for the given provider name.\n */\nexport function createProviderInstance(\n name: ProviderName,\n config: Config\n): Provider | null {\n const providerConfig = config.providers[name];\n\n switch (name) {\n case 'pubmed': {\n if (!providerConfig.email) {\n const configPath = getConfigDir();\n console.warn(\n `Warning: No email configured for PubMed.\\n` +\n ` → Edit ${configPath}/config.toml and set providers.pubmed.email\\n` +\n ` → Or run: search-hub config providers.pubmed.email \"your@email.com\"`\n );\n }\n const pubmedOpts: PubMedConfig = {\n email: providerConfig.email ?? 'search-hub@example.com',\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n };\n if (providerConfig.api_key) {\n pubmedOpts.apiKey = providerConfig.api_key;\n }\n return new PubMedProvider(pubmedOpts);\n }\n case 'eric':\n return new ERICProvider({\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n });\n case 'arxiv':\n return new ArxivProvider({\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n });\n case 'scopus': {\n if (!providerConfig.api_key) {\n console.warn(\n `Warning: Scopus requires an API key. Set providers.scopus.api_key in config.\\n` +\n ` → Get an API key at https://dev.elsevier.com/\\n` +\n ` → Run: search-hub config providers.scopus.api_key \"your-key\"`\n );\n return null;\n }\n const scopusOpts: ScopusConfig = {\n apiKey: providerConfig.api_key,\n rateLimit: providerConfig.rate_limit,\n timeout: providerConfig.timeout,\n retries: providerConfig.retries,\n };\n if (providerConfig.inst_token) {\n scopusOpts.instToken = providerConfig.inst_token;\n }\n return new ScopusProvider(scopusOpts);\n }\n default:\n throw new Error(`Provider '${name}' is not implemented`);\n }\n}\n\n/**\n * Translate a query AST for a specific provider.\n * Resolves provider-specific blocks/filters before translation.\n */\nfunction translateQueryForProvider(\n ast: QueryAST,\n provider: ProviderName\n): TranslatedQuery {\n const resolved = resolveForProvider(ast, provider);\n switch (provider) {\n case 'pubmed':\n return translatePubmed(resolved);\n case 'eric':\n return translateEric(resolved);\n case 'arxiv':\n return translateArxiv(resolved);\n case 'scopus':\n return translateScopus(resolved);\n default:\n throw new Error(`No translator for provider '${provider}'`);\n }\n}\n\n/**\n * Get enabled providers from config, optionally filtered by user selection.\n */\nfunction getEnabledProviders(\n config: Config,\n requestedProviders?: ProviderName[]\n): ProviderName[] {\n const enabledInConfig = IMPLEMENTED_PROVIDERS.filter(\n (name) => config.providers[name].enabled\n );\n\n if (requestedProviders && requestedProviders.length > 0) {\n return requestedProviders.filter((p) => enabledInConfig.includes(p));\n }\n\n return enabledInConfig;\n}\n\n/**\n * Execute a search across multiple providers.\n */\nexport async function executeSearch(\n options: SearchCommandOptions,\n sessionsDir: string,\n config: Config,\n showProgress = true\n): Promise<SearchExecutionResult> {\n let ast: QueryAST | undefined;\n let queryContent: string;\n let queryFile: string;\n\n // Handle direct query mode\n if (options.directQuery && options.providers && options.providers.length === 1) {\n queryFile = 'direct-query';\n\n // For direct query, we create a minimal AST structure\n ast = {\n name: options.sessionName ?? 'direct-query',\n blocks: [\n {\n id: 'direct',\n field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n providers: {},\n };\n\n // Generate YAML safely using yaml library to handle special characters\n queryContent = stringifyYaml({\n name: ast.name,\n blocks: ast.blocks,\n filters: ast.filters,\n });\n } else if (options.queryFile) {\n // Parse query file\n try {\n queryContent = await readFile(options.queryFile, 'utf-8');\n ast = parseQueryString(queryContent);\n queryFile = options.queryFile;\n } catch (error) {\n return {\n success: false,\n sessionStatus: 'failed',\n error: `Failed to parse query file: ${error instanceof Error ? error.message : error}`,\n };\n }\n } else {\n return {\n success: false,\n sessionStatus: 'failed',\n error: 'Either queryFile or directQuery with provider is required',\n };\n }\n\n // Determine which providers to use\n let providers = getEnabledProviders(config, options.providers);\n\n // In default mode (no --db), skip unconfigured providers with a warning\n const isExplicitSelection = options.providers && options.providers.length > 0;\n if (!isExplicitSelection) {\n const skipped: ProviderName[] = [];\n providers = providers.filter((name) => {\n if (!isProviderConfigured(name, config)) {\n skipped.push(name);\n return false;\n }\n return true;\n });\n for (const name of skipped) {\n console.warn(\n `Skipping ${name}: API key not configured (use --db ${name} to force)`\n );\n }\n }\n\n if (providers.length === 0) {\n return {\n success: false,\n sessionStatus: 'failed',\n error: 'No providers enabled or selected',\n };\n }\n\n // Create query hash\n const queryHash = createHash('sha256').update(queryContent).digest('hex').slice(0, 8);\n\n // Create session\n let session;\n try {\n const sessionOpts: Parameters<typeof createSession>[0] = {\n name: options.sessionName ?? ast.name,\n queryFile,\n queryContent,\n queryHash,\n targets: providers,\n sessionsDir,\n };\n if (ast.description) {\n sessionOpts.description = ast.description;\n }\n session = await createSession(sessionOpts);\n } catch (error) {\n return {\n success: false,\n sessionStatus: 'failed',\n error: `Failed to create session: ${error instanceof Error ? error.message : error}`,\n };\n }\n\n const sessionId = session.id;\n const results: Record<string, { hits: number; retrieved: number; error?: string; warnings?: string[] }> = {};\n\n // Create progress display if enabled\n let progress: MultiProviderProgress | undefined;\n if (showProgress && process.stdout.isTTY) {\n progress = new MultiProviderProgress(providers);\n }\n\n // Execute search for each provider\n for (const providerName of providers) {\n try {\n // Create provider instance\n const provider = createProviderInstance(providerName, config);\n\n // Skip provider if it could not be created (e.g. missing configuration)\n if (provider === null) {\n const configError = `${providerName}: provider configuration incomplete. See warning above for details.`;\n results[providerName] = { hits: 0, retrieved: 0, error: configError };\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'failed',\n completedAt: new Date().toISOString(),\n error: {\n code: 'CONFIG_ERROR',\n message: configError,\n retryable: false,\n },\n },\n sessionsDir\n );\n continue;\n }\n\n // Translate query\n let translatedQuery: TranslatedQuery;\n if (options.directQuery && options.providers?.length === 1) {\n // For direct query, use the native query string directly\n translatedQuery = {\n native: options.directQuery,\n provider: providerName,\n };\n } else {\n translatedQuery = translateQueryForProvider(ast, providerName);\n }\n\n // Write translated query to session\n const queryPath = join(sessionsDir, sessionId, `${providerName}_query.txt`);\n await writeFile(queryPath, translatedQuery.native, 'utf-8');\n\n // Update database status to in_progress\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'in_progress',\n startedAt: new Date().toISOString(),\n },\n sessionsDir\n );\n\n // Prepare results file path\n const resultsPath = join(sessionsDir, sessionId, `${providerName}_results.jsonl`);\n\n // Execute search\n let retrievedCount = 0;\n let totalHits = 0;\n\n progress?.update(providerName, 0, 0, 'in_progress');\n\n const searchOptions = {\n maxResults: options.maxResults ?? config.providers[providerName].max_results,\n ...(options.sort && { sort: options.sort }),\n };\n\n for await (const article of provider.search(translatedQuery, searchOptions)) {\n retrievedCount++;\n\n // Write article to JSONL file\n await appendFile(resultsPath, JSON.stringify(article) + '\\n', 'utf-8');\n\n // Update progress (estimate total from first batch)\n if (totalHits === 0) {\n // Estimate total - this is provider-dependent, we'll use retrieved count as minimum\n totalHits = Math.max(retrievedCount * 10, 100);\n }\n progress?.update(providerName, retrievedCount, totalHits, 'in_progress');\n }\n\n // Update final totals\n totalHits = retrievedCount; // Use actual count as final total\n\n // Mark as completed\n progress?.complete(providerName);\n\n // Convert JSONL to YAML for human-readable view\n const yamlFilename = `${providerName}_results.yaml`;\n const yamlPath = join(sessionsDir, sessionId, yamlFilename);\n await convertResultsToYaml(resultsPath, yamlPath, {\n provider: providerName,\n queryName: ast.name,\n });\n\n // Update database status\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'completed',\n completedAt: new Date().toISOString(),\n totalHits,\n retrievedCount,\n files: {\n query: `${providerName}_query.txt`,\n results: `${providerName}_results.jsonl`,\n resultsYaml: yamlFilename,\n },\n },\n sessionsDir\n );\n\n // Collect warnings if provider supports them\n const providerWarnings = provider.getWarnings?.();\n results[providerName] = { hits: totalHits, retrieved: retrievedCount, ...(providerWarnings && providerWarnings.length > 0 && { warnings: providerWarnings }) };\n } catch (error) {\n const errorMessage = error instanceof Error\n ? error.message\n : isProviderError(error)\n ? error.message\n : String(error);\n\n progress?.fail(providerName, errorMessage);\n\n // Update database status with error\n await updateDatabaseStatus(\n sessionId,\n providerName,\n {\n status: 'failed',\n completedAt: new Date().toISOString(),\n error: {\n code: 'SEARCH_ERROR',\n message: errorMessage,\n retryable: true,\n },\n },\n sessionsDir\n );\n\n results[providerName] = { hits: 0, retrieved: 0, error: errorMessage };\n }\n }\n\n // Stop progress display\n progress?.stop();\n\n // Determine overall session status\n const anyFailed = providers.some((p) => {\n const r = results[p];\n return r && r.error !== undefined;\n });\n const anySucceeded = providers.some((p) => {\n const r = results[p];\n return r && r.retrieved > 0;\n });\n\n let sessionStatus: 'completed' | 'partial' | 'failed';\n if (!anyFailed) {\n sessionStatus = 'completed';\n } else if (anySucceeded) {\n sessionStatus = 'partial';\n } else {\n sessionStatus = 'failed';\n }\n\n // Update session status\n await updateSessionStatus(sessionId, sessionStatus, sessionsDir);\n\n if (sessionStatus === 'failed') {\n return {\n success: false,\n sessionId,\n sessionStatus,\n results,\n error: buildFailureErrorMessage(results),\n };\n }\n\n // In strict mode, partial success is treated as failure\n if (options.strict && sessionStatus === 'partial') {\n return {\n success: false,\n sessionId,\n sessionStatus,\n results,\n error: buildPartialErrorMessage(results),\n };\n }\n\n // Auto-register if enabled\n let autoRegisterResult: RegistrationRecord | undefined;\n if (\n config.integration.reference_manager.enabled &&\n config.integration.reference_manager.auto_register\n ) {\n const refAvailable = await checkRefAvailable();\n if (refAvailable) {\n // Load all articles from results files\n const allArticles = await loadArticlesFromSession(sessionsDir, sessionId, providers);\n\n if (allArticles.length > 0) {\n autoRegisterResult = await registerArticles(allArticles, {\n sessionId,\n sessionDir: join(sessionsDir, sessionId),\n withAbstracts: config.integration.reference_manager.with_abstracts,\n });\n\n // Save registration record\n await saveRegistrationRecord(join(sessionsDir, sessionId), autoRegisterResult);\n }\n }\n }\n\n const result: SearchExecutionResult = {\n success: true,\n sessionId,\n results,\n sessionStatus,\n };\n\n if (autoRegisterResult) {\n result.autoRegisterResult = autoRegisterResult;\n }\n\n return result;\n}\n\n/**\n * Execute count-only mode: get hit counts from each provider without downloading results.\n * No session is created.\n */\nexport async function executeCountOnly(\n options: SearchCommandOptions,\n config: Config\n): Promise<CountResult[]> {\n let ast: QueryAST | undefined;\n\n // Handle direct query mode\n if (options.directQuery && options.providers && options.providers.length === 1) {\n ast = {\n name: options.sessionName ?? 'direct-query',\n blocks: [\n {\n id: 'direct',\n field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n providers: {},\n };\n } else if (options.queryFile) {\n const queryContent = await readFile(options.queryFile, 'utf-8');\n ast = parseQueryString(queryContent);\n } else {\n return [];\n }\n\n // Determine which providers to use\n let providers = getEnabledProviders(config, options.providers);\n\n // In default mode (no --db), skip unconfigured providers\n const isExplicitSelection = options.providers && options.providers.length > 0;\n if (!isExplicitSelection) {\n providers = providers.filter((name) => isProviderConfigured(name, config));\n }\n\n if (providers.length === 0) {\n return [];\n }\n\n // Execute count for each provider concurrently\n const results: CountResult[] = await Promise.all(\n providers.map(async (providerName): Promise<CountResult> => {\n try {\n const provider = createProviderInstance(providerName, config);\n if (provider === null) {\n return { provider: providerName, count: 0, error: 'Provider configuration incomplete' };\n }\n\n // Translate query\n let translatedQuery: TranslatedQuery;\n if (options.directQuery && options.providers?.length === 1) {\n translatedQuery = {\n native: options.directQuery,\n provider: providerName,\n };\n } else {\n translatedQuery = translateQueryForProvider(ast!, providerName);\n }\n\n const count = await provider.count(translatedQuery);\n return { provider: providerName, count };\n } catch (error) {\n const errorMessage = error instanceof Error\n ? error.message\n : isProviderError(error)\n ? error.message\n : String(error);\n return { provider: providerName, count: 0, error: errorMessage };\n }\n })\n );\n\n return results;\n}\n\n\n/**\n * Execute preview mode: get counts and first few titles without creating a session.\n */\nexport async function executePreview(\n options: SearchCommandOptions,\n config: Config,\n maxTitles = 5\n): Promise<PreviewResult[]> {\n let ast: QueryAST | undefined;\n\n // Handle direct query mode\n if (options.directQuery && options.providers && options.providers.length === 1) {\n ast = {\n name: options.sessionName ?? 'direct-query',\n blocks: [\n {\n id: 'direct',\n field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n providers: {},\n };\n } else if (options.queryFile) {\n const queryContent = await readFile(options.queryFile, 'utf-8');\n ast = parseQueryString(queryContent);\n } else {\n return [];\n }\n\n // Determine which providers to use\n let providers = getEnabledProviders(config, options.providers);\n\n // In default mode (no --db), skip unconfigured providers\n const isExplicitSelection = options.providers && options.providers.length > 0;\n if (!isExplicitSelection) {\n providers = providers.filter((name) => isProviderConfigured(name, config));\n }\n\n if (providers.length === 0) {\n return [];\n }\n\n // Execute preview for each provider concurrently\n const results: PreviewResult[] = await Promise.all(\n providers.map(async (providerName): Promise<PreviewResult> => {\n try {\n const provider = createProviderInstance(providerName, config);\n if (provider === null) {\n return { provider: providerName, count: 0, titles: [], error: 'Provider configuration incomplete' };\n }\n\n // Translate query\n let translatedQuery: TranslatedQuery;\n if (options.directQuery && options.providers?.length === 1) {\n translatedQuery = {\n native: options.directQuery,\n provider: providerName,\n };\n } else {\n translatedQuery = translateQueryForProvider(ast!, providerName);\n }\n\n // Get count first\n const count = await provider.count(translatedQuery);\n\n // Collect first few articles for titles\n const titles: string[] = [];\n const searchOptions = { maxResults: maxTitles };\n\n for await (const article of provider.search(translatedQuery, searchOptions)) {\n titles.push(article.title);\n if (titles.length >= maxTitles) {\n break;\n }\n }\n\n return { provider: providerName, count, titles };\n } catch (error) {\n const errorMessage = error instanceof Error\n ? error.message\n : isProviderError(error)\n ? error.message\n : String(error);\n return { provider: providerName, count: 0, titles: [], error: errorMessage };\n }\n })\n );\n\n return results;\n}\n\n/**\n * Load all articles from a session's results files (YAML preferred, JSONL fallback).\n */\nasync function loadArticlesFromSession(\n sessionsDir: string,\n sessionId: string,\n providers: ProviderName[]\n): Promise<Article[]> {\n const articles: Article[] = [];\n const sessionDir = join(sessionsDir, sessionId);\n\n for (const provider of providers) {\n const providerArticles = await loadResults(sessionDir, provider);\n articles.push(...providerArticles);\n }\n\n return articles;\n}\n"],"names":["translatePubmed","translateEric","translateArxiv","translateScopus","stringifyYaml"],"mappings":"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"}
@@ -7,6 +7,7 @@ export interface SearchCommandOptions {
7
7
  providers?: ProviderName[];
8
8
  sessionName?: string;
9
9
  maxResults?: number;
10
+ sort?: 'relevance' | 'date';
10
11
  dryRun?: boolean;
11
12
  countOnly?: boolean;
12
13
  preview?: boolean;
@@ -18,6 +19,7 @@ export interface CommandLineOptions {
18
19
  query?: string | undefined;
19
20
  name?: string | undefined;
20
21
  maxResults?: string | undefined;
22
+ sort?: string | undefined;
21
23
  dryRun?: boolean | undefined;
22
24
  countOnly?: boolean | undefined;
23
25
  preview?: boolean | undefined;
@@ -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;AAuM5C;;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,CA+tFvC;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";
@@ -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
@@ -1595,12 +1716,16 @@ Examples:
1595
1716
  $ search-hub review extract --session SESSION_ID --name title-screening # Extract for review
1596
1717
  $ search-hub review merge --session SESSION_ID --name title-screening # Merge reviews
1597
1718
  $ 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) => {
1719
+ 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
1720
  const globalOpts = program.opts();
1600
1721
  try {
1722
+ if (options.mode && options.mode !== "screening" && options.mode !== "picking") {
1723
+ throw new Error(`Invalid mode: "${options.mode}". Must be "screening" or "picking".`);
1724
+ }
1601
1725
  const sessionsDir = await getSessionsDir(globalOpts);
1602
1726
  const initOptions = {
1603
1727
  sessionId: options.session,
1728
+ ...options.mode && { mode: options.mode },
1604
1729
  ...options.force && { force: options.force }
1605
1730
  };
1606
1731
  const result = await executeReviewInit(initOptions, sessionsDir);
@@ -1648,10 +1773,10 @@ Examples:
1648
1773
  process.exitCode = EXIT_CODES.SESSION_ERROR;
1649
1774
  }
1650
1775
  });
1651
- reviewCommand.command("list").description("List articles with optional filtering").requiredOption("--session <id>", "session ID").option("--filter <type>", "filter by status: pending, incomplete, uncertain, agreed-include, agreed-exclude, conflicting, finalized, all", "all").option("--json", "output as JSON").action(async (options) => {
1776
+ reviewCommand.command("list").description("List articles with optional filtering").requiredOption("--session <id>", "session ID").option("--filter <type>", "filter by status: pending, incomplete, all-uncertain, agreed-include, agreed-exclude, divided, finalized, all", "all").option("--json", "output as JSON").action(async (options) => {
1652
1777
  const globalOpts = program.opts();
1653
1778
  try {
1654
- const validFilters = ["pending", "incomplete", "uncertain", "agreed-include", "agreed-exclude", "conflicting", "finalized", "all"];
1779
+ const validFilters = ["pending", "incomplete", "all-uncertain", "agreed-include", "agreed-exclude", "divided", "finalized", "all"];
1655
1780
  const filter = options.filter ?? "all";
1656
1781
  if (!validFilters.includes(filter)) {
1657
1782
  if (!globalOpts.quiet) {
@@ -1681,7 +1806,7 @@ Examples:
1681
1806
  process.exitCode = EXIT_CODES.SESSION_ERROR;
1682
1807
  }
1683
1808
  });
1684
- reviewCommand.command("extract").description("Extract subset to for-review/<name>/review.yaml for distributed review").requiredOption("--session <id>", "session ID").requiredOption("--name <name>", "name for the review subset (output: for-review/<name>/review.yaml)").option("--filter <types>", "filter by status (comma-separated): pending, incomplete, uncertain, agreed-include, agreed-exclude, conflicting, finalized").option("--sort <method>", "sort method: year, title, random, none", "none").option("--limit <n>", "limit number of articles").option("--offset <n>", "skip first n articles").option("--seed <n>", "random seed for reproducible sorting").option("--basis <type>", "basis for review: title, abstract, or fulltext").option("--reviewer <id>", 'reviewer identifier (e.g., "ai:claude")').option("--finalize", "extract for final decision (includes reviewHistory and finalDecision)").action(async (options) => {
1809
+ reviewCommand.command("extract").description("Extract subset to for-review/<name>/review.yaml for distributed review").requiredOption("--session <id>", "session ID").requiredOption("--name <name>", "name for the review subset (output: for-review/<name>/review.yaml)").option("--filter <types>", "filter by status (comma-separated): pending, incomplete, all-uncertain, agreed-include, agreed-exclude, divided, finalized").option("--sort <method>", "sort method: year, title, random, none", "none").option("--limit <n>", "limit number of articles").option("--offset <n>", "skip first n articles").option("--seed <n>", "random seed for reproducible sorting").option("--basis <type>", "basis for review: title, abstract, or fulltext").option("--reviewer <id>", 'reviewer identifier (e.g., "ai:claude")').option("--finalize", "extract for final decision (includes reviewHistory and finalDecision)").action(async (options) => {
1685
1810
  const globalOpts = program.opts();
1686
1811
  try {
1687
1812
  const validSorts = ["year", "title", "random", "none"];