@ncukondo/search-hub 0.13.0 → 0.15.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 (100) hide show
  1. package/README.md +39 -9
  2. package/dist/cli/commands/check.d.ts +34 -0
  3. package/dist/cli/commands/check.d.ts.map +1 -0
  4. package/dist/cli/commands/check.js +126 -0
  5. package/dist/cli/commands/check.js.map +1 -0
  6. package/dist/cli/commands/export.d.ts +5 -3
  7. package/dist/cli/commands/export.d.ts.map +1 -1
  8. package/dist/cli/commands/export.js +0 -4
  9. package/dist/cli/commands/export.js.map +1 -1
  10. package/dist/cli/commands/query/init.d.ts.map +1 -1
  11. package/dist/cli/commands/query/init.js +17 -7
  12. package/dist/cli/commands/query/init.js.map +1 -1
  13. package/dist/cli/commands/query/inspect.d.ts +36 -0
  14. package/dist/cli/commands/query/inspect.d.ts.map +1 -0
  15. package/dist/cli/commands/query/inspect.js +155 -0
  16. package/dist/cli/commands/query/inspect.js.map +1 -0
  17. package/dist/cli/commands/query/translate.d.ts.map +1 -1
  18. package/dist/cli/commands/query/translate.js +3 -1
  19. package/dist/cli/commands/query/translate.js.map +1 -1
  20. package/dist/cli/commands/query-filter.d.ts +13 -0
  21. package/dist/cli/commands/query-filter.d.ts.map +1 -0
  22. package/dist/cli/commands/query-filter.js +149 -0
  23. package/dist/cli/commands/query-filter.js.map +1 -0
  24. package/dist/cli/commands/results.d.ts +3 -3
  25. package/dist/cli/commands/results.d.ts.map +1 -1
  26. package/dist/cli/commands/results.js +12 -3
  27. package/dist/cli/commands/results.js.map +1 -1
  28. package/dist/cli/commands/search-executor.d.ts.map +1 -1
  29. package/dist/cli/commands/search-executor.js +12 -7
  30. package/dist/cli/commands/search-executor.js.map +1 -1
  31. package/dist/cli/e2e-helpers.d.ts +5 -2
  32. package/dist/cli/e2e-helpers.d.ts.map +1 -1
  33. package/dist/cli/index.d.ts.map +1 -1
  34. package/dist/cli/index.js +228 -45
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/index.js +4 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/providers/arxiv/provider.d.ts +3 -3
  39. package/dist/providers/arxiv/provider.d.ts.map +1 -1
  40. package/dist/providers/arxiv/provider.js +3 -3
  41. package/dist/providers/arxiv/provider.js.map +1 -1
  42. package/dist/providers/arxiv/translator.d.ts +3 -3
  43. package/dist/providers/arxiv/translator.d.ts.map +1 -1
  44. package/dist/providers/arxiv/translator.js +6 -8
  45. package/dist/providers/arxiv/translator.js.map +1 -1
  46. package/dist/providers/base/index.d.ts +1 -1
  47. package/dist/providers/base/index.d.ts.map +1 -1
  48. package/dist/providers/base/mock-provider.d.ts +2 -2
  49. package/dist/providers/base/mock-provider.d.ts.map +1 -1
  50. package/dist/providers/base/mock-provider.js +2 -9
  51. package/dist/providers/base/mock-provider.js.map +1 -1
  52. package/dist/providers/base/provider.d.ts +3 -3
  53. package/dist/providers/base/provider.d.ts.map +1 -1
  54. package/dist/providers/base/provider.js.map +1 -1
  55. package/dist/providers/base/types.d.ts +4 -6
  56. package/dist/providers/base/types.d.ts.map +1 -1
  57. package/dist/providers/base/types.js.map +1 -1
  58. package/dist/providers/eric/provider.d.ts +3 -3
  59. package/dist/providers/eric/provider.d.ts.map +1 -1
  60. package/dist/providers/eric/provider.js +3 -3
  61. package/dist/providers/eric/provider.js.map +1 -1
  62. package/dist/providers/eric/translator.d.ts +5 -5
  63. package/dist/providers/eric/translator.d.ts.map +1 -1
  64. package/dist/providers/eric/translator.js +6 -7
  65. package/dist/providers/eric/translator.js.map +1 -1
  66. package/dist/providers/pubmed/provider.d.ts +3 -3
  67. package/dist/providers/pubmed/provider.d.ts.map +1 -1
  68. package/dist/providers/pubmed/provider.js +3 -3
  69. package/dist/providers/pubmed/provider.js.map +1 -1
  70. package/dist/providers/pubmed/translator.d.ts +3 -3
  71. package/dist/providers/pubmed/translator.d.ts.map +1 -1
  72. package/dist/providers/pubmed/translator.js +4 -23
  73. package/dist/providers/pubmed/translator.js.map +1 -1
  74. package/dist/providers/scopus/provider.d.ts +3 -3
  75. package/dist/providers/scopus/provider.d.ts.map +1 -1
  76. package/dist/providers/scopus/provider.js +3 -3
  77. package/dist/providers/scopus/provider.js.map +1 -1
  78. package/dist/providers/scopus/translator.d.ts +3 -3
  79. package/dist/providers/scopus/translator.d.ts.map +1 -1
  80. package/dist/providers/scopus/translator.js +7 -9
  81. package/dist/providers/scopus/translator.js.map +1 -1
  82. package/dist/query/index.d.ts +3 -2
  83. package/dist/query/index.d.ts.map +1 -1
  84. package/dist/query/json-schema.d.ts.map +1 -1
  85. package/dist/query/json-schema.js +20 -11
  86. package/dist/query/json-schema.js.map +1 -1
  87. package/dist/query/mesh-lookup.d.ts.map +1 -1
  88. package/dist/query/mesh-lookup.js +66 -3
  89. package/dist/query/mesh-lookup.js.map +1 -1
  90. package/dist/query/resolver.d.ts +14 -0
  91. package/dist/query/resolver.d.ts.map +1 -0
  92. package/dist/query/resolver.js +61 -0
  93. package/dist/query/resolver.js.map +1 -0
  94. package/dist/query/types.d.ts +31 -11
  95. package/dist/query/types.d.ts.map +1 -1
  96. package/dist/query/validator.d.ts +659 -348
  97. package/dist/query/validator.d.ts.map +1 -1
  98. package/dist/query/validator.js +70 -30
  99. package/dist/query/validator.js.map +1 -1
  100. package/package.json +1 -1
@@ -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 {\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 */\nfunction translateQueryForProvider(\n ast: QueryAST,\n provider: ProviderName\n): TranslatedQuery {\n switch (provider) {\n case 'pubmed':\n return translatePubmed(ast);\n case 'eric':\n return translateEric(ast);\n case 'arxiv':\n return translateArxiv(ast);\n case 'scopus':\n return translateScopus(ast);\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 field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n overrides: {},\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 field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n overrides: {},\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 field: 'all',\n terms: { keywords: [options.directQuery] },\n operator: 'AND',\n },\n ],\n filters: {},\n overrides: {},\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 };\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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"}
@@ -115,12 +115,14 @@ export declare const queryFixtures: {
115
115
  name: string;
116
116
  description: string;
117
117
  blocks: ({
118
+ id: string;
118
119
  field: "title_abstract";
119
120
  terms: {
120
121
  keywords: string[];
121
122
  };
122
123
  operator: "AND";
123
124
  } | {
125
+ id: string;
124
126
  field: "keyword";
125
127
  terms: {
126
128
  keywords: string[];
@@ -132,12 +134,13 @@ export declare const queryFixtures: {
132
134
  yearTo: number;
133
135
  languages: string[];
134
136
  };
135
- overrides: {};
137
+ providers: {};
136
138
  };
137
139
  /** Query with MeSH terms (PubMed-specific) */
138
140
  withMesh: {
139
141
  name: string;
140
142
  blocks: {
143
+ id: string;
141
144
  field: "title_abstract";
142
145
  terms: {
143
146
  keywords: string[];
@@ -146,7 +149,7 @@ export declare const queryFixtures: {
146
149
  operator: "AND";
147
150
  }[];
148
151
  filters: {};
149
- overrides: {};
152
+ providers: {};
150
153
  };
151
154
  };
152
155
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"e2e-helpers.d.ts","sourceRoot":"","sources":["../../src/cli/e2e-helpers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"e2e-helpers.d.ts","sourceRoot":"","sources":["../../src/cli/e2e-helpers.ts"],"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;AAiLpC;;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,CAkzEvC;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,EAAE,MAAM,WAAW,CAAC;AAuLpC;;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,CAygFvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
package/dist/cli/index.js CHANGED
@@ -16,6 +16,7 @@ import { VocabCache } from "../query/vocab-cache.js";
16
16
  import { createEricCountValidator, createEmtreeCountValidator } from "../query/vocab-validator.js";
17
17
  import { createProviderInstance, executePreview, executeCountOnly, executeSearch } from "./commands/search-executor.js";
18
18
  import { translateQueryCommand, formatTranslateResult } from "./commands/query/translate.js";
19
+ import { inspectQueryCommand, formatInspectOutput } from "./commands/query/inspect.js";
19
20
  import { writeQueryTemplate, generateQueryTemplate } from "./commands/query/init.js";
20
21
  import { getSessionDetails, computeDeduplicationStats, formatSessionDetails, listSessionsForDisplay, formatSessionList } from "./commands/status.js";
21
22
  import { parseSearchOptions, validateSearchInput, formatShortKeywordWarning, formatDryRunOutput, formatPreviewOutput, formatCountOnlyOutput } from "./commands/search.js";
@@ -25,6 +26,7 @@ import { formatVerboseProviderDetails } from "./commands/search-utils.js";
25
26
  import { parseExportOptions, validateExportInput, deduplicateArticles, filterArticles, formatIds, formatJson, formatCslJson, formatJsonl } from "./commands/export.js";
26
27
  import { computeSummary, formatSummaryJson, formatSummary } from "./commands/summary.js";
27
28
  import { parseResultsOptions, validateResultsInput, formatResultsJson, formatResultsList } from "./commands/results.js";
29
+ import { filterByQuery } from "./commands/query-filter.js";
28
30
  import { loadNotes, formatAllSessionNotes, formatNotesList, addNote, addAssessment } from "./commands/notes.js";
29
31
  import { computeDiff, computeQueryDiff, formatDiffJson, formatDiff } from "./commands/diff.js";
30
32
  import { validateMergeSources, mergeArticles, formatMergeJson, formatMergeOutput, createMergedSession } from "./commands/merge.js";
@@ -51,6 +53,7 @@ import { fileURLToPath } from "node:url";
51
53
  import { getSessionsDir } from "./utils/sessions-dir.js";
52
54
  import { expandPath } from "../utils/path.js";
53
55
  import { loadSessionArticles, loadSessionQuery } from "./commands/session-utils.js";
56
+ import { parseIdentifierFile, checkCoverage, formatCheckResultJson, formatCheckResult } from "./commands/check.js";
54
57
  config({ quiet: true });
55
58
  function createProgram() {
56
59
  const program = new Command();
@@ -60,11 +63,11 @@ function createProgram() {
60
63
  Workflow:
61
64
  1. query init → edit → validate / --dry-run Query preparation
62
65
  2. search --preview → search Preview & execute
63
- 3. results / summary / diff Inspect & compare
66
+ 3. results / summary / diff / check Inspect & verify
64
67
  4. review init → extract → merge → status Systematic review
65
68
  5. register / export Output
66
69
 
67
- Iterate: search v1 search v2 → diff Query refinement
70
+ Iterate: search → results -qcheck → diff Query refinement
68
71
 
69
72
  Quick Start:
70
73
  $ search-hub query init -o search.yaml # Create query template
@@ -288,6 +291,36 @@ Examples:
288
291
  process.exitCode = EXIT_CODES.QUERY_ERROR;
289
292
  }
290
293
  });
294
+ queryCommand.command("inspect").description("Show how a query resolves per provider (block replacements and added filters)").argument("<file>", "path to query YAML file").option("--db <provider>", "show resolution for specific provider only").addHelpText("after", `
295
+ Examples:
296
+ $ search-hub query inspect ./diabetes-ai.yaml # All databases
297
+ $ search-hub query inspect ./diabetes-ai.yaml --db pubmed # PubMed only`).action(async (file, options) => {
298
+ const globalOpts = program.opts();
299
+ try {
300
+ const inspectOptions = options.db ? { providers: [options.db] } : {};
301
+ const result = await inspectQueryCommand(file, inspectOptions);
302
+ if (!result.success) {
303
+ if (!globalOpts.quiet) {
304
+ console.error(`✗ Failed to inspect: ${file}
305
+ Error: ${result.error}`);
306
+ }
307
+ process.exitCode = EXIT_CODES.QUERY_ERROR;
308
+ return;
309
+ }
310
+ if (!globalOpts.quiet) {
311
+ console.log(formatInspectOutput(result.result));
312
+ }
313
+ process.exitCode = EXIT_CODES.SUCCESS;
314
+ } catch (error) {
315
+ if (!globalOpts.quiet) {
316
+ console.error(
317
+ "Error:",
318
+ error instanceof Error ? error.message : error
319
+ );
320
+ }
321
+ process.exitCode = EXIT_CODES.QUERY_ERROR;
322
+ }
323
+ });
291
324
  queryCommand.command("init").description("Generate a template query YAML file").option("-o, --output <path>", "write to file (default: stdout)").option("--force", "overwrite existing file", false).action(async (options) => {
292
325
  const globalOpts = program.opts();
293
326
  try {
@@ -376,7 +409,7 @@ Query features (use "query init" to see full template):
376
409
  filters: year_from, year_to, language, publication_types
377
410
  exclude: NOT terms per block (terms.exclude)
378
411
  mesh/eric: controlled vocabulary (terms.mesh, terms.eric)
379
- overrides: per-database settings (pubmed, scopus, eric, arxiv)`).action(
412
+ providers: per-database block replacements and filter additions`).action(
380
413
  async (queryFile, options) => {
381
414
  const globalOpts = program.opts();
382
415
  try {
@@ -695,25 +728,44 @@ Resume completed. ${execResult.resumed} provider(s) resumed.`);
695
728
  }
696
729
  }
697
730
  );
698
- program.command("export").description("Export session results to various formats").argument("<session-id>", "session ID to export").option("--format <fmt>", "output format: ids, json, jsonl, csl-json", "jsonl").option("-o, --output <path>", "output file path (default: stdout)").option("--db <providers>", "export only specific database(s)").option("--id-type <type>", "for ids format: doi, pmid, all").option("--no-dedup", "disable deduplication of results").option("--filter-year <range>", 'year range filter (e.g., "2023-2025")').option("--filter-title <keywords>", "title keyword filter (comma-separated)").option("--filter-abstract <keywords>", "abstract keyword filter (comma-separated)").addHelpText("after", `
731
+ program.command("export").description("Export session results to various formats").argument("<session-id>", "session ID to export").option("--format <fmt>", "output format: ids, json, jsonl, csl-json", "jsonl").option("-o, --output <path>", "output file path (default: stdout)").option("--id-type <type>", "for ids format: doi, pmid, all").option("--no-dedup", "disable deduplication of results").option("-q, --query <expr>", "filter results with query expression").option("--filter-year <range>", "year range filter (deprecated, use -q)").option("--filter-title <keywords>", "title keyword filter (deprecated, use -q)").option("--filter-abstract <keywords>", "abstract keyword filter (deprecated, use -q)").addHelpText("after", `
699
732
  Examples:
700
733
  $ search-hub export SESSION_ID # JSONL to stdout
701
734
  $ search-hub export SESSION_ID --format json # JSON to stdout
735
+ $ search-hub export SESSION_ID -q "year:2023" # Filter by query
736
+ $ search-hub export SESSION_ID -q "author:smith" --format ids # Filtered IDs
702
737
  $ search-hub export SESSION_ID --format json -o results.json # JSON to file
703
738
  $ search-hub export SESSION_ID --format ids --id-type doi # Export DOIs to stdout
704
- $ search-hub export SESSION_ID --format csl-json -o refs.json # CSL-JSON to file
705
- $ search-hub export SESSION_ID --db pubmed --format jsonl
706
739
  $ search-hub export SESSION_ID --no-dedup # Export without deduplication
707
740
  $ search-hub export SESSION_ID --format jsonl | jq '.title' # Pipe to jq
708
- $ search-hub export SESSION_ID --filter-year 2023-2025 # Filter by year
709
- $ search-hub export SESSION_ID --filter-title "machine learning,AI" # Filter by title`).action(
741
+
742
+ Query syntax:
743
+ Free text diabetes Search title and abstract
744
+ title:VALUE title:learning Title substring
745
+ abstract:VALUE abstract:randomized Abstract substring
746
+ author:VALUE author:tanaka Author name substring
747
+ journal:VALUE journal:lancet Journal name substring
748
+ year:VALUE year:2023 Exact year
749
+ year:FROM-TO year:2020-2024 Year range
750
+ doi:VALUE doi:10.1001/xxx DOI exact match
751
+ pmid:VALUE pmid:12345678 PMID exact match
752
+ source:VALUE source:pubmed Provider exact match
753
+
754
+ Multiple terms: different fields = AND, same field = OR`).action(
710
755
  async (sessionId, options) => {
711
756
  const globalOpts = program.opts();
712
757
  try {
758
+ const hasLegacyFilter = options?.filterYear || options?.filterTitle || options?.filterAbstract;
759
+ if (options?.query && hasLegacyFilter) {
760
+ if (!globalOpts.quiet) {
761
+ console.error("Error: Cannot use -q/--query together with --filter-year, --filter-title, or --filter-abstract. Use -q only.");
762
+ }
763
+ process.exitCode = EXIT_CODES.SESSION_ERROR;
764
+ return;
765
+ }
713
766
  const exportOpts = parseExportOptions(sessionId, {
714
767
  format: options?.format,
715
768
  output: options?.output,
716
- db: options?.db,
717
769
  idType: options?.idType
718
770
  });
719
771
  const validation = validateExportInput(exportOpts);
@@ -737,7 +789,7 @@ Examples:
737
789
  process.exitCode = EXIT_CODES.SESSION_ERROR;
738
790
  return;
739
791
  }
740
- const articles = await loadSessionArticles(session, sessionId, sessionsDir, exportOpts.providers);
792
+ const articles = await loadSessionArticles(session, sessionId, sessionsDir);
741
793
  const shouldDedup = options?.dedup !== false;
742
794
  let exportArticles;
743
795
  let duplicatesRemoved = 0;
@@ -748,32 +800,38 @@ Examples:
748
800
  } else {
749
801
  exportArticles = articles;
750
802
  }
751
- const filter = {};
752
- if (options?.filterYear) {
753
- const parts = options.filterYear.split("-");
754
- if (parts.length === 2) {
755
- const from = parseInt(parts[0], 10);
756
- const to = parseInt(parts[1], 10);
757
- if (!Number.isNaN(from)) filter.yearFrom = from;
758
- if (!Number.isNaN(to)) filter.yearTo = to;
759
- } else if (parts.length === 1) {
760
- const year = parseInt(parts[0], 10);
761
- if (!Number.isNaN(year)) {
762
- filter.yearFrom = year;
763
- filter.yearTo = year;
803
+ const preFilterCount = exportArticles.length;
804
+ let hasFilter = false;
805
+ if (options?.query) {
806
+ exportArticles = filterByQuery(exportArticles, options.query);
807
+ hasFilter = true;
808
+ } else {
809
+ const filter = {};
810
+ if (options?.filterYear) {
811
+ const parts = options.filterYear.split("-");
812
+ if (parts.length === 2) {
813
+ const from = parseInt(parts[0], 10);
814
+ const to = parseInt(parts[1], 10);
815
+ if (!Number.isNaN(from)) filter.yearFrom = from;
816
+ if (!Number.isNaN(to)) filter.yearTo = to;
817
+ } else if (parts.length === 1) {
818
+ const year = parseInt(parts[0], 10);
819
+ if (!Number.isNaN(year)) {
820
+ filter.yearFrom = year;
821
+ filter.yearTo = year;
822
+ }
764
823
  }
765
824
  }
766
- }
767
- if (options?.filterTitle) {
768
- filter.titleKeywords = options.filterTitle.split(",").map((s) => s.trim()).filter(Boolean);
769
- }
770
- if (options?.filterAbstract) {
771
- filter.abstractKeywords = options.filterAbstract.split(",").map((s) => s.trim()).filter(Boolean);
772
- }
773
- const preFilterCount = exportArticles.length;
774
- const hasFilter = filter.yearFrom !== void 0 || filter.yearTo !== void 0 || filter.titleKeywords && filter.titleKeywords.length > 0 || filter.abstractKeywords && filter.abstractKeywords.length > 0;
775
- if (hasFilter) {
776
- exportArticles = filterArticles(exportArticles, filter);
825
+ if (options?.filterTitle) {
826
+ filter.titleKeywords = options.filterTitle.split(",").map((s) => s.trim()).filter(Boolean);
827
+ }
828
+ if (options?.filterAbstract) {
829
+ filter.abstractKeywords = options.filterAbstract.split(",").map((s) => s.trim()).filter(Boolean);
830
+ }
831
+ hasFilter = !!(filter.yearFrom !== void 0 || filter.yearTo !== void 0 || filter.titleKeywords && filter.titleKeywords.length > 0 || filter.abstractKeywords && filter.abstractKeywords.length > 0);
832
+ if (hasFilter) {
833
+ exportArticles = filterArticles(exportArticles, filter);
834
+ }
777
835
  }
778
836
  let output;
779
837
  if (exportOpts.format === "ids") {
@@ -881,15 +939,30 @@ Examples:
881
939
  }
882
940
  }
883
941
  );
884
- program.command("results").description("List articles from a session with title, year, and journal").argument("<session-id>", "session ID to list results from").option("--limit <n>", "maximum number of results to show").option("--offset <n>", "skip first n results").option("--json", "output as JSON array").option("--fields <fields>", "fields to display (comma-separated)").option("--db <providers>", "filter by database(s), comma-separated").option("--filter-year <range>", 'year range filter (e.g., "2023-2025")').option("--filter-title <keywords>", "title keyword filter (comma-separated)").option("--filter-abstract <keywords>", "abstract keyword filter (comma-separated)").option("--abstract", "show abstracts with results").option("--abstract-length <n>", "maximum abstract length in characters (default: 300)").addHelpText("after", `
942
+ program.command("results").description("List articles from a session with title, year, and journal").argument("<session-id>", "session ID to list results from").option("--limit <n>", "maximum number of results to show").option("--offset <n>", "skip first n results").option("--json", "output as JSON array").option("--fields <fields>", "fields to display (comma-separated)").option("-q, --query <expr>", "filter results with query expression").option("--filter-year <range>", "year range filter (deprecated, use -q)").option("--filter-title <keywords>", "title keyword filter (deprecated, use -q)").option("--filter-abstract <keywords>", "abstract keyword filter (deprecated, use -q)").option("--abstract", "show abstracts with results").option("--abstract-length <n>", "maximum abstract length in characters (default: 300)").addHelpText("after", `
885
943
  Examples:
886
- $ search-hub results SESSION_ID # List all articles
887
- $ search-hub results SESSION_ID --limit 20 # First 20 articles
888
- $ search-hub results SESSION_ID --limit 20 --offset 40 # Articles 41-60
889
- $ search-hub results SESSION_ID --json # JSON output for scripting
890
- $ search-hub results SESSION_ID --db pubmed # Only PubMed articles
891
- $ search-hub results SESSION_ID --filter-year 2023-2025 # Filter by year
892
- $ search-hub results SESSION_ID --abstract # Show with abstracts`).action(
944
+ $ search-hub results SESSION_ID # List all articles
945
+ $ search-hub results SESSION_ID --limit 20 # First 20 articles
946
+ $ search-hub results SESSION_ID -q "diabetes" # Free text filter
947
+ $ search-hub results SESSION_ID -q "author:smith year:2023" # Combined filter
948
+ $ search-hub results SESSION_ID -q "doi:10.1001/xxx" # Exact ID match
949
+ $ search-hub results SESSION_ID --json # JSON output
950
+ $ search-hub results SESSION_ID -q "source:pubmed" # Only PubMed
951
+ $ search-hub results SESSION_ID --abstract # Show abstracts
952
+
953
+ Query syntax:
954
+ Free text diabetes Search title and abstract
955
+ title:VALUE title:learning Title substring
956
+ abstract:VALUE abstract:randomized Abstract substring
957
+ author:VALUE author:tanaka Author name substring
958
+ journal:VALUE journal:lancet Journal name substring
959
+ year:VALUE year:2023 Exact year
960
+ year:FROM-TO year:2020-2024 Year range
961
+ doi:VALUE doi:10.1001/xxx DOI exact match
962
+ pmid:VALUE pmid:12345678 PMID exact match
963
+ source:VALUE source:pubmed Provider exact match
964
+
965
+ Multiple terms: different fields = AND, same field = OR`).action(
893
966
  async (sessionId, options) => {
894
967
  const globalOpts = program.opts();
895
968
  try {
@@ -898,7 +971,7 @@ Examples:
898
971
  offset: options?.offset,
899
972
  json: options?.json,
900
973
  fields: options?.fields,
901
- db: options?.db,
974
+ query: options?.query,
902
975
  filterYear: options?.filterYear,
903
976
  filterTitle: options?.filterTitle,
904
977
  filterAbstract: options?.filterAbstract,
@@ -926,11 +999,17 @@ Examples:
926
999
  process.exitCode = EXIT_CODES.SESSION_ERROR;
927
1000
  return;
928
1001
  }
929
- const articles = await loadSessionArticles(session, sessionId, sessionsDir, resultsOpts.providers);
1002
+ const articles = await loadSessionArticles(session, sessionId, sessionsDir);
930
1003
  const dedupResult = deduplicateArticles(articles);
931
1004
  let displayArticles = dedupResult.articles;
932
1005
  let filteredFrom;
933
- if (resultsOpts.filter) {
1006
+ if (resultsOpts.query) {
1007
+ const preFilterCount = displayArticles.length;
1008
+ displayArticles = filterByQuery(displayArticles, resultsOpts.query);
1009
+ if (displayArticles.length !== preFilterCount) {
1010
+ filteredFrom = preFilterCount;
1011
+ }
1012
+ } else if (resultsOpts.filter) {
934
1013
  const preFilterCount = displayArticles.length;
935
1014
  displayArticles = filterArticles(displayArticles, resultsOpts.filter);
936
1015
  if (displayArticles.length !== preFilterCount) {
@@ -1072,6 +1151,110 @@ Query Refinement Workflow:
1072
1151
  }
1073
1152
  }
1074
1153
  );
1154
+ program.command("check").description("Verify coverage of known articles against session results").argument("<session-id>", "session ID to check against").option("--file <path>", "file with identifiers (one per line)").option("--doi <ids>", "comma-separated DOIs to check").option("--pmid <ids>", "comma-separated PMIDs to check").option("--json", "output as JSON").option("--missing-only", "show only missing identifiers").addHelpText("after", `
1155
+ Examples:
1156
+ $ search-hub check SESSION --file known-dois.txt # Check from file
1157
+ $ search-hub check SESSION --doi "10.1001/jama.2023.12345" # Check single DOI
1158
+ $ search-hub check SESSION --pmid "37654321,36543210" # Check PMIDs
1159
+ $ search-hub check SESSION --file refs.txt --json # JSON output
1160
+ $ search-hub check SESSION --file refs.txt --missing-only # Only missing
1161
+
1162
+ Input file format (one identifier per line):
1163
+ 10.1001/jama.2023.12345 DOI (starts with "10.")
1164
+ 37654321 PMID (numeric only)
1165
+ DOI:10.1038/s41586-023-xxxxx DOI (explicit prefix)
1166
+ PMID:36543210 PMID (explicit prefix)
1167
+ arxiv:2301.12345 arXiv ID (explicit prefix)
1168
+ # comment Comments and empty lines ignored`).action(
1169
+ async (sessionId, options) => {
1170
+ const globalOpts = program.opts();
1171
+ try {
1172
+ let identifiers;
1173
+ let source;
1174
+ if (options?.file) {
1175
+ const filePath = expandPath(options.file);
1176
+ let content;
1177
+ try {
1178
+ content = await readFile(filePath, "utf-8");
1179
+ } catch {
1180
+ if (!globalOpts.quiet) {
1181
+ console.error(`Error: File not found: ${filePath}`);
1182
+ }
1183
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
1184
+ return;
1185
+ }
1186
+ try {
1187
+ identifiers = parseIdentifierFile(content);
1188
+ } catch (error) {
1189
+ if (!globalOpts.quiet) {
1190
+ console.error(`Error: ${error instanceof Error ? error.message : "Failed to parse identifier file"}`);
1191
+ }
1192
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
1193
+ return;
1194
+ }
1195
+ source = options.file;
1196
+ } else if (options?.doi || options?.pmid) {
1197
+ const lines = [];
1198
+ if (options.doi) {
1199
+ lines.push(...options.doi.split(",").map((d) => d.trim()).filter(Boolean));
1200
+ }
1201
+ if (options.pmid) {
1202
+ lines.push(...options.pmid.split(",").map((p) => `PMID:${p.trim()}`).filter(Boolean));
1203
+ }
1204
+ identifiers = parseIdentifierFile(lines.join("\n"));
1205
+ source = "command line";
1206
+ } else {
1207
+ if (!globalOpts.quiet) {
1208
+ console.error("Error: Provide --file, --doi, or --pmid");
1209
+ }
1210
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
1211
+ return;
1212
+ }
1213
+ if (identifiers.length === 0) {
1214
+ if (!globalOpts.quiet) {
1215
+ console.error("Error: No identifiers found in input");
1216
+ }
1217
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
1218
+ return;
1219
+ }
1220
+ const sessionsDir = await getSessionsDir(globalOpts);
1221
+ let session;
1222
+ try {
1223
+ session = await loadSession(sessionId, sessionsDir);
1224
+ } catch (error) {
1225
+ if (!globalOpts.quiet) {
1226
+ console.error(
1227
+ `Error: ${error instanceof Error ? error.message : "Failed to load session"}`
1228
+ );
1229
+ }
1230
+ process.exitCode = EXIT_CODES.SESSION_ERROR;
1231
+ return;
1232
+ }
1233
+ const articles = await loadSessionArticles(session, sessionId, sessionsDir);
1234
+ const result = checkCoverage(articles, identifiers);
1235
+ if (options?.json) {
1236
+ console.log(formatCheckResultJson(result, { sessionId, source }));
1237
+ } else {
1238
+ if (!globalOpts.quiet) {
1239
+ console.log(formatCheckResult(result, {
1240
+ sessionId,
1241
+ source,
1242
+ missingOnly: options?.missingOnly
1243
+ }));
1244
+ }
1245
+ }
1246
+ process.exitCode = EXIT_CODES.SUCCESS;
1247
+ } catch (error) {
1248
+ if (!globalOpts.quiet) {
1249
+ console.error(
1250
+ "Error:",
1251
+ error instanceof Error ? error.message : error
1252
+ );
1253
+ }
1254
+ process.exitCode = EXIT_CODES.GENERAL_ERROR;
1255
+ }
1256
+ }
1257
+ );
1075
1258
  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", `
1076
1259
  Examples:
1077
1260
  $ search-hub merge session-v4 session-v9 # Merge two sessions