@ncukondo/search-hub 0.1.3 → 0.1.4

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 (47) hide show
  1. package/dist/cli/commands/query/init.d.ts +22 -0
  2. package/dist/cli/commands/query/init.d.ts.map +1 -0
  3. package/dist/cli/commands/query/init.js +67 -0
  4. package/dist/cli/commands/query/init.js.map +1 -0
  5. package/dist/cli/commands/resume-executor.d.ts +1 -0
  6. package/dist/cli/commands/resume-executor.d.ts.map +1 -1
  7. package/dist/cli/commands/resume-executor.js +27 -5
  8. package/dist/cli/commands/resume-executor.js.map +1 -1
  9. package/dist/cli/commands/search-executor.d.ts +3 -1
  10. package/dist/cli/commands/search-executor.d.ts.map +1 -1
  11. package/dist/cli/commands/search-executor.js +41 -7
  12. package/dist/cli/commands/search-executor.js.map +1 -1
  13. package/dist/cli/commands/search-utils.d.ts +21 -0
  14. package/dist/cli/commands/search-utils.d.ts.map +1 -0
  15. package/dist/cli/commands/search-utils.js +29 -0
  16. package/dist/cli/commands/search-utils.js.map +1 -0
  17. package/dist/cli/commands/search.d.ts +17 -1
  18. package/dist/cli/commands/search.d.ts.map +1 -1
  19. package/dist/cli/commands/search.js +73 -8
  20. package/dist/cli/commands/search.js.map +1 -1
  21. package/dist/cli/index.d.ts.map +1 -1
  22. package/dist/cli/index.js +70 -18
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/config/env.d.ts.map +1 -1
  25. package/dist/config/env.js +2 -0
  26. package/dist/config/env.js.map +1 -1
  27. package/dist/config/schema.d.ts +21 -21
  28. package/dist/config/schema.d.ts.map +1 -1
  29. package/dist/config/schema.js +5 -3
  30. package/dist/config/schema.js.map +1 -1
  31. package/dist/providers/pubmed/client.d.ts +2 -1
  32. package/dist/providers/pubmed/client.d.ts.map +1 -1
  33. package/dist/providers/pubmed/client.js +5 -9
  34. package/dist/providers/pubmed/client.js.map +1 -1
  35. package/dist/providers/pubmed/parser.d.ts.map +1 -1
  36. package/dist/providers/pubmed/parser.js +18 -1
  37. package/dist/providers/pubmed/parser.js.map +1 -1
  38. package/dist/providers/pubmed/provider.d.ts +6 -0
  39. package/dist/providers/pubmed/provider.d.ts.map +1 -1
  40. package/dist/providers/pubmed/provider.js +33 -19
  41. package/dist/providers/pubmed/provider.js.map +1 -1
  42. package/dist/providers/pubmed/translator.d.ts.map +1 -1
  43. package/dist/providers/pubmed/translator.js +17 -3
  44. package/dist/providers/pubmed/translator.js.map +1 -1
  45. package/dist/providers/pubmed/types.d.ts +2 -0
  46. package/dist/providers/pubmed/types.d.ts.map +1 -1
  47. package/package.json +4 -2
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Generate the query template YAML string.
3
+ *
4
+ * @returns The YAML template string with comments
5
+ */
6
+ export declare function generateQueryTemplate(): string;
7
+ /**
8
+ * Write the query template to a file or return it as a message.
9
+ *
10
+ * @param options - Output options
11
+ * @param options.output - File path to write to (if omitted, returns template in message)
12
+ * @param options.force - Whether to overwrite existing files
13
+ * @returns Result with success status and message
14
+ */
15
+ export declare function writeQueryTemplate(options: {
16
+ output?: string;
17
+ force?: boolean;
18
+ }): Promise<{
19
+ success: boolean;
20
+ message: string;
21
+ }>;
22
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"AAkDA;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA4BjD"}
@@ -0,0 +1,67 @@
1
+ import { access, writeFile } from "node:fs/promises";
2
+ const QUERY_TEMPLATE = `name: my_search
3
+ description: ""
4
+
5
+ query:
6
+ - field: title_abstract # title, abstract, title_abstract, author, keyword, all
7
+ terms:
8
+ keywords:
9
+ - "search term 1"
10
+ - "search term 2"
11
+ # mesh: # PubMed MeSH terms (optional)
12
+ # - "MeSH Heading"
13
+ operator: OR # How to combine terms within this block
14
+
15
+ # Add more blocks — blocks are AND'd together
16
+ # - field: title_abstract
17
+ # terms:
18
+ # keywords:
19
+ # - "another term"
20
+ # operator: OR
21
+
22
+ # filters: # Optional: apply to all databases
23
+ # year_from: 2020
24
+ # year_to: 2026
25
+ # language:
26
+ # - en
27
+ # publication_types:
28
+ # exclude:
29
+ # - "Review"
30
+ # - "Comment"
31
+
32
+ # overrides: # Optional: database-specific settings
33
+ # pubmed:
34
+ # filters:
35
+ # publication_types:
36
+ # exclude:
37
+ # - "Letter"
38
+ `;
39
+ function generateQueryTemplate() {
40
+ return QUERY_TEMPLATE;
41
+ }
42
+ async function writeQueryTemplate(options) {
43
+ const template = generateQueryTemplate();
44
+ if (!options.output) {
45
+ return { success: true, message: template };
46
+ }
47
+ if (!options.force) {
48
+ try {
49
+ await access(options.output);
50
+ return {
51
+ success: false,
52
+ message: `File already exists: ${options.output}. Use --force to overwrite.`
53
+ };
54
+ } catch {
55
+ }
56
+ }
57
+ await writeFile(options.output, template, "utf-8");
58
+ return {
59
+ success: true,
60
+ message: `Template written to ${options.output}`
61
+ };
62
+ }
63
+ export {
64
+ generateQueryTemplate,
65
+ writeQueryTemplate
66
+ };
67
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sources":["../../../../src/cli/commands/query/init.ts"],"sourcesContent":["/**\n * Query init command implementation.\n *\n * Generates a skeleton YAML query file with helpful comments.\n */\nimport { writeFile as fsWriteFile, access } from \"node:fs/promises\";\n\n/**\n * The YAML template string with comments preserved.\n * This is a raw string (not generated by a YAML library) so comments are kept.\n */\n// prettier-ignore\nconst QUERY_TEMPLATE =\n \"name: my_search\\n\" +\n \"description: \\\"\\\"\\n\" +\n \"\\n\" +\n \"query:\\n\" +\n \" - field: title_abstract # title, abstract, title_abstract, author, keyword, all\\n\" +\n \" terms:\\n\" +\n \" keywords:\\n\" +\n \" - \\\"search term 1\\\"\\n\" +\n \" - \\\"search term 2\\\"\\n\" +\n \" # mesh: # PubMed MeSH terms (optional)\\n\" +\n \" # - \\\"MeSH Heading\\\"\\n\" +\n \" operator: OR # How to combine terms within this block\\n\" +\n \"\\n\" +\n \" # Add more blocks — blocks are AND'd together\\n\" +\n \" # - field: title_abstract\\n\" +\n \" # terms:\\n\" +\n \" # keywords:\\n\" +\n \" # - \\\"another term\\\"\\n\" +\n \" # operator: OR\\n\" +\n \"\\n\" +\n \"# filters: # Optional: apply to all databases\\n\" +\n \"# year_from: 2020\\n\" +\n \"# year_to: 2026\\n\" +\n \"# language:\\n\" +\n \"# - en\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Review\\\"\\n\" +\n \"# - \\\"Comment\\\"\\n\" +\n \"\\n\" +\n \"# overrides: # Optional: database-specific settings\\n\" +\n \"# pubmed:\\n\" +\n \"# filters:\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Letter\\\"\\n\";\n\n/**\n * Generate the query template YAML string.\n *\n * @returns The YAML template string with comments\n */\nexport function generateQueryTemplate(): string {\n return QUERY_TEMPLATE;\n}\n\n/**\n * Write the query template to a file or return it as a message.\n *\n * @param options - Output options\n * @param options.output - File path to write to (if omitted, returns template in message)\n * @param options.force - Whether to overwrite existing files\n * @returns Result with success status and message\n */\nexport async function writeQueryTemplate(options: {\n output?: string;\n force?: boolean;\n}): Promise<{ success: boolean; message: string }> {\n const template = generateQueryTemplate();\n\n if (!options.output) {\n // No output file specified, return template as message\n return { success: true, message: template };\n }\n\n // Check if file exists (unless force is set)\n if (!options.force) {\n try {\n await access(options.output);\n // File exists and force is not set\n return {\n success: false,\n message: `File already exists: ${options.output}. Use --force to overwrite.`,\n };\n } catch {\n // File does not exist, proceed\n }\n }\n\n // Write to file\n await fsWriteFile(options.output, template, \"utf-8\");\n return {\n success: true,\n message: `Template written to ${options.output}`,\n };\n}\n"],"names":["fsWriteFile"],"mappings":";AAYA,MAAM,iBACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA0CK,SAAS,wBAAgC;AAC9C,SAAO;AACT;AAUA,eAAsB,mBAAmB,SAGU;AACjD,QAAM,WAAW,sBAAA;AAEjB,MAAI,CAAC,QAAQ,QAAQ;AAEnB,WAAO,EAAE,SAAS,MAAM,SAAS,SAAA;AAAA,EACnC;AAGA,MAAI,CAAC,QAAQ,OAAO;AAClB,QAAI;AACF,YAAM,OAAO,QAAQ,MAAM;AAE3B,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,wBAAwB,QAAQ,MAAM;AAAA,MAAA;AAAA,IAEnD,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAMA,UAAY,QAAQ,QAAQ,UAAU,OAAO;AACnD,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS,uBAAuB,QAAQ,MAAM;AAAA,EAAA;AAElD;"}
@@ -9,6 +9,7 @@ export interface ResumeExecutionResult {
9
9
  results?: Record<string, {
10
10
  hits: number;
11
11
  retrieved: number;
12
+ error?: string;
12
13
  }>;
13
14
  error?: string;
14
15
  }
@@ -1 +1 @@
1
- {"version":3,"file":"resume-executor.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/resume-executor.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAepD;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAgBD;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,EAC7B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,UAAO,GAClB,OAAO,CAAC,qBAAqB,CAAC,CAyNhC"}
1
+ {"version":3,"file":"resume-executor.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/resume-executor.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAiBpD;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAgBD;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,EAC7B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,UAAO,GAClB,OAAO,CAAC,qBAAqB,CAAC,CAmPhC"}
@@ -1,8 +1,10 @@
1
1
  import { readFile, appendFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { isProviderError } from "../../providers/base/types.js";
3
4
  import { loadSession, getResumableProviders, updateDatabaseStatus, updateSessionStatus } from "../../session/manager.js";
4
5
  import { MultiProviderProgress } from "../utils/progress.js";
5
6
  import { createProviderInstance } from "./search-executor.js";
7
+ import { buildFailureErrorMessage } from "./search-utils.js";
6
8
  function isResumable(provider) {
7
9
  return "resumeSearch" in provider && typeof provider.resumeSearch === "function";
8
10
  }
@@ -45,13 +47,33 @@ async function executeResume(options, sessionsDir, config, showProgress = true)
45
47
  if (!dbStatus) continue;
46
48
  try {
47
49
  const provider = createProviderInstance(providerName, config);
50
+ if (provider === null) {
51
+ progress?.fail(providerName, "Provider configuration missing");
52
+ const configError = `${providerName}: provider configuration incomplete. See warning above for details.`;
53
+ results[providerName] = { hits: 0, retrieved: 0, error: configError };
54
+ await updateDatabaseStatus(
55
+ options.sessionId,
56
+ providerName,
57
+ {
58
+ status: "failed",
59
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
60
+ error: {
61
+ code: "CONFIG_ERROR",
62
+ message: configError,
63
+ retryable: false
64
+ }
65
+ },
66
+ sessionsDir
67
+ );
68
+ continue;
69
+ }
48
70
  const queryPath = join(sessionsDir, options.sessionId, dbStatus.files.query);
49
71
  let nativeQuery;
50
72
  try {
51
73
  nativeQuery = await readFile(queryPath, "utf-8");
52
74
  } catch {
53
75
  progress?.fail(providerName, "Query file not found");
54
- results[providerName] = { hits: 0, retrieved: 0 };
76
+ results[providerName] = { hits: 0, retrieved: 0, error: "Query file not found" };
55
77
  continue;
56
78
  }
57
79
  const translatedQuery = {
@@ -113,7 +135,7 @@ async function executeResume(options, sessionsDir, config, showProgress = true)
113
135
  results[providerName] = { hits: totalHits || retrievedCount, retrieved: retrievedCount };
114
136
  successCount++;
115
137
  } catch (error) {
116
- const errorMessage = error instanceof Error ? error.message : String(error);
138
+ const errorMessage = error instanceof Error ? error.message : isProviderError(error) ? error.message : String(error);
117
139
  progress?.fail(providerName, errorMessage);
118
140
  await updateDatabaseStatus(
119
141
  options.sessionId,
@@ -129,13 +151,13 @@ async function executeResume(options, sessionsDir, config, showProgress = true)
129
151
  },
130
152
  sessionsDir
131
153
  );
132
- results[providerName] = { hits: 0, retrieved: 0 };
154
+ results[providerName] = { hits: 0, retrieved: 0, error: errorMessage };
133
155
  }
134
156
  }
135
157
  progress?.stop();
136
158
  const anyFailed = resumableProviders.some((p) => {
137
159
  const r = results[p.provider];
138
- return r && r.retrieved === 0 && r.hits === 0;
160
+ return r && r.error !== void 0;
139
161
  });
140
162
  const anySucceeded = resumableProviders.some((p) => {
141
163
  const r = results[p.provider];
@@ -155,7 +177,7 @@ async function executeResume(options, sessionsDir, config, showProgress = true)
155
177
  success: false,
156
178
  resumed: successCount,
157
179
  results,
158
- error: "All providers failed"
180
+ error: buildFailureErrorMessage(results)
159
181
  };
160
182
  }
161
183
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"resume-executor.js","sources":["../../../src/cli/commands/resume-executor.ts"],"sourcesContent":["/**\n * Resume executor for CLI resume command.\n *\n * Handles resuming interrupted search sessions by continuing\n * from where providers left off or retrying failed providers.\n */\nimport { readFile, appendFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { ResumeCommandOptions } from './resume.js';\nimport type { Config } from '../../config/index.js';\nimport type {\n Provider,\n TranslatedQuery,\n SearchState,\n} from '../../providers/base/types.js';\nimport {\n loadSession,\n updateDatabaseStatus,\n updateSessionStatus,\n getResumableProviders,\n} from '../../session/manager.js';\nimport { MultiProviderProgress } from '../utils/progress.js';\nimport { createProviderInstance } from './search-executor.js';\n\n/**\n * Result of a resume execution.\n */\nexport interface ResumeExecutionResult {\n success: boolean;\n resumed: number;\n results?: Record<string, { hits: number; retrieved: number }>;\n error?: string;\n}\n\n/**\n * Interface for providers that support resuming.\n */\ninterface ResumableProviderInstance extends Provider {\n resumeSearch(state: SearchState): AsyncIterable<import('../../providers/base/types.js').Article>;\n}\n\n/**\n * Check if a provider instance supports resuming.\n */\nfunction isResumable(provider: Provider): provider is ResumableProviderInstance {\n return 'resumeSearch' in provider && typeof (provider as any).resumeSearch === 'function';\n}\n\n/**\n * Execute resume for interrupted sessions.\n */\nexport async function executeResume(\n options: ResumeCommandOptions,\n sessionsDir: string,\n config: Config,\n showProgress = true\n): Promise<ResumeExecutionResult> {\n // Load session\n let session;\n try {\n session = await loadSession(options.sessionId, sessionsDir);\n } catch (error) {\n return {\n success: false,\n resumed: 0,\n error: `Failed to load session: ${error instanceof Error ? error.message : error}`,\n };\n }\n\n // Get resumable providers\n let resumableProviders = getResumableProviders(session);\n\n // Filter by specific providers if requested\n if (options.providers && options.providers.length > 0) {\n resumableProviders = resumableProviders.filter((r) =>\n options.providers!.includes(r.provider)\n );\n }\n\n // Filter to only retry strategies if retryFailed is true\n if (options.retryFailed) {\n resumableProviders = resumableProviders.filter((r) => r.strategy === 'retry');\n }\n\n if (resumableProviders.length === 0) {\n return {\n success: true,\n resumed: 0,\n error: 'No providers to resume',\n };\n }\n\n const results: Record<string, { hits: number; retrieved: number }> = {};\n\n // Create progress display if enabled\n let progress: MultiProviderProgress | undefined;\n if (showProgress && process.stdout.isTTY) {\n progress = new MultiProviderProgress(resumableProviders.map((p) => p.provider));\n }\n\n let successCount = 0;\n\n // Resume each provider\n for (const resumable of resumableProviders) {\n const providerName = resumable.provider;\n const dbStatus = session.databases[providerName];\n\n if (!dbStatus) continue;\n\n try {\n // Create provider instance\n const provider = createProviderInstance(providerName, config);\n\n // Build the translated query from stored query file\n const queryPath = join(sessionsDir, options.sessionId, dbStatus.files.query);\n let nativeQuery: string;\n try {\n nativeQuery = await readFile(queryPath, 'utf-8');\n } catch {\n // If query file doesn't exist, skip this provider\n progress?.fail(providerName, 'Query file not found');\n results[providerName] = { hits: 0, retrieved: 0 };\n continue;\n }\n\n const translatedQuery: TranslatedQuery = {\n native: nativeQuery.trim(),\n provider: providerName,\n };\n\n // Update database status to in_progress\n await updateDatabaseStatus(\n options.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, options.sessionId, dbStatus.files.results);\n\n let retrievedCount = dbStatus.retrievedCount ?? 0;\n const totalHits = dbStatus.totalHits ?? 0;\n\n progress?.update(providerName, retrievedCount, totalHits || 100, 'in_progress');\n\n // Determine how to resume\n if (resumable.strategy === 'continue' && isResumable(provider)) {\n // Build SearchState for continuing\n const searchState: SearchState = {\n provider: providerName,\n query: translatedQuery,\n totalResults: totalHits,\n retrievedCount,\n lastUpdated: new Date(),\n providerState: dbStatus.pagination\n ? {\n cursor: dbStatus.pagination.cursor,\n pageNumber: dbStatus.pagination.pageNumber,\n }\n : undefined,\n };\n\n // Resume search\n for await (const article of provider.resumeSearch(searchState)) {\n retrievedCount++;\n\n // Write article to JSONL file\n await appendFile(resultsPath, JSON.stringify(article) + '\\n', 'utf-8');\n\n progress?.update(providerName, retrievedCount, totalHits || retrievedCount, 'in_progress');\n }\n } else {\n // Fresh start or retry - use regular search\n const searchOptions = {\n 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 progress?.update(providerName, retrievedCount, totalHits || retrievedCount * 2, 'in_progress');\n }\n }\n\n // Mark as completed\n progress?.complete(providerName);\n\n // Update database status\n await updateDatabaseStatus(\n options.sessionId,\n providerName,\n {\n status: 'completed',\n completedAt: new Date().toISOString(),\n totalHits: totalHits || retrievedCount,\n retrievedCount,\n },\n sessionsDir\n );\n\n results[providerName] = { hits: totalHits || retrievedCount, retrieved: retrievedCount };\n successCount++;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n progress?.fail(providerName, errorMessage);\n\n // Update database status with error\n await updateDatabaseStatus(\n options.sessionId,\n providerName,\n {\n status: 'failed',\n completedAt: new Date().toISOString(),\n error: {\n code: 'RESUME_ERROR',\n message: errorMessage,\n retryable: true,\n },\n },\n sessionsDir\n );\n\n results[providerName] = { hits: 0, retrieved: 0 };\n }\n }\n\n // Stop progress display\n progress?.stop();\n\n // Determine overall session status\n const anyFailed = resumableProviders.some((p) => {\n const r = results[p.provider];\n return r && r.retrieved === 0 && r.hits === 0;\n });\n const anySucceeded = resumableProviders.some((p) => {\n const r = results[p.provider];\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(options.sessionId, sessionStatus, sessionsDir);\n\n if (sessionStatus === 'failed') {\n return {\n success: false,\n resumed: successCount,\n results,\n error: 'All providers failed',\n };\n }\n\n return {\n success: true,\n resumed: successCount,\n results,\n };\n}\n"],"names":[],"mappings":";;;;;AA4CA,SAAS,YAAY,UAA2D;AAC9E,SAAO,kBAAkB,YAAY,OAAQ,SAAiB,iBAAiB;AACjF;AAKA,eAAsB,cACpB,SACA,aACA,QACA,eAAe,MACiB;AAEhC,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,YAAY,QAAQ,WAAW,WAAW;AAAA,EAC5D,SAAS,OAAO;AACd,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS;AAAA,MACT,OAAO,2BAA2B,iBAAiB,QAAQ,MAAM,UAAU,KAAK;AAAA,IAAA;AAAA,EAEpF;AAGA,MAAI,qBAAqB,sBAAsB,OAAO;AAGtD,MAAI,QAAQ,aAAa,QAAQ,UAAU,SAAS,GAAG;AACrD,yBAAqB,mBAAmB;AAAA,MAAO,CAAC,MAC9C,QAAQ,UAAW,SAAS,EAAE,QAAQ;AAAA,IAAA;AAAA,EAE1C;AAGA,MAAI,QAAQ,aAAa;AACvB,yBAAqB,mBAAmB,OAAO,CAAC,MAAM,EAAE,aAAa,OAAO;AAAA,EAC9E;AAEA,MAAI,mBAAmB,WAAW,GAAG;AACnC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AAEA,QAAM,UAA+D,CAAA;AAGrE,MAAI;AACJ,MAAI,gBAAgB,QAAQ,OAAO,OAAO;AACxC,eAAW,IAAI,sBAAsB,mBAAmB,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC;AAAA,EAChF;AAEA,MAAI,eAAe;AAGnB,aAAW,aAAa,oBAAoB;AAC1C,UAAM,eAAe,UAAU;AAC/B,UAAM,WAAW,QAAQ,UAAU,YAAY;AAE/C,QAAI,CAAC,SAAU;AAEf,QAAI;AAEF,YAAM,WAAW,uBAAuB,cAAc,MAAM;AAG5D,YAAM,YAAY,KAAK,aAAa,QAAQ,WAAW,SAAS,MAAM,KAAK;AAC3E,UAAI;AACJ,UAAI;AACF,sBAAc,MAAM,SAAS,WAAW,OAAO;AAAA,MACjD,QAAQ;AAEN,kBAAU,KAAK,cAAc,sBAAsB;AACnD,gBAAQ,YAAY,IAAI,EAAE,MAAM,GAAG,WAAW,EAAA;AAC9C;AAAA,MACF;AAEA,YAAM,kBAAmC;AAAA,QACvC,QAAQ,YAAY,KAAA;AAAA,QACpB,UAAU;AAAA,MAAA;AAIZ,YAAM;AAAA,QACJ,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,YAAW,oBAAI,KAAA,GAAO,YAAA;AAAA,QAAY;AAAA,QAEpC;AAAA,MAAA;AAIF,YAAM,cAAc,KAAK,aAAa,QAAQ,WAAW,SAAS,MAAM,OAAO;AAE/E,UAAI,iBAAiB,SAAS,kBAAkB;AAChD,YAAM,YAAY,SAAS,aAAa;AAExC,gBAAU,OAAO,cAAc,gBAAgB,aAAa,KAAK,aAAa;AAG9E,UAAI,UAAU,aAAa,cAAc,YAAY,QAAQ,GAAG;AAE9D,cAAM,cAA2B;AAAA,UAC/B,UAAU;AAAA,UACV,OAAO;AAAA,UACP,cAAc;AAAA,UACd;AAAA,UACA,iCAAiB,KAAA;AAAA,UACjB,eAAe,SAAS,aACpB;AAAA,YACE,QAAQ,SAAS,WAAW;AAAA,YAC5B,YAAY,SAAS,WAAW;AAAA,UAAA,IAElC;AAAA,QAAA;AAIN,yBAAiB,WAAW,SAAS,aAAa,WAAW,GAAG;AAC9D;AAGA,gBAAM,WAAW,aAAa,KAAK,UAAU,OAAO,IAAI,MAAM,OAAO;AAErE,oBAAU,OAAO,cAAc,gBAAgB,aAAa,gBAAgB,aAAa;AAAA,QAC3F;AAAA,MACF,OAAO;AAEL,cAAM,gBAAgB;AAAA,UACpB,YAAY,OAAO,UAAU,YAAY,EAAE;AAAA,QAAA;AAG7C,yBAAiB,WAAW,SAAS,OAAO,iBAAiB,aAAa,GAAG;AAC3E;AAGA,gBAAM,WAAW,aAAa,KAAK,UAAU,OAAO,IAAI,MAAM,OAAO;AAErE,oBAAU,OAAO,cAAc,gBAAgB,aAAa,iBAAiB,GAAG,aAAa;AAAA,QAC/F;AAAA,MACF;AAGA,gBAAU,SAAS,YAAY;AAG/B,YAAM;AAAA,QACJ,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,cAAa,oBAAI,KAAA,GAAO,YAAA;AAAA,UACxB,WAAW,aAAa;AAAA,UACxB;AAAA,QAAA;AAAA,QAEF;AAAA,MAAA;AAGF,cAAQ,YAAY,IAAI,EAAE,MAAM,aAAa,gBAAgB,WAAW,eAAA;AACxE;AAAA,IACF,SAAS,OAAO;AACd,YAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAE1E,gBAAU,KAAK,cAAc,YAAY;AAGzC,YAAM;AAAA,QACJ,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,cAAa,oBAAI,KAAA,GAAO,YAAA;AAAA,UACxB,OAAO;AAAA,YACL,MAAM;AAAA,YACN,SAAS;AAAA,YACT,WAAW;AAAA,UAAA;AAAA,QACb;AAAA,QAEF;AAAA,MAAA;AAGF,cAAQ,YAAY,IAAI,EAAE,MAAM,GAAG,WAAW,EAAA;AAAA,IAChD;AAAA,EACF;AAGA,YAAU,KAAA;AAGV,QAAM,YAAY,mBAAmB,KAAK,CAAC,MAAM;AAC/C,UAAM,IAAI,QAAQ,EAAE,QAAQ;AAC5B,WAAO,KAAK,EAAE,cAAc,KAAK,EAAE,SAAS;AAAA,EAC9C,CAAC;AACD,QAAM,eAAe,mBAAmB,KAAK,CAAC,MAAM;AAClD,UAAM,IAAI,QAAQ,EAAE,QAAQ;AAC5B,WAAO,KAAK,EAAE,YAAY;AAAA,EAC5B,CAAC;AAED,MAAI;AACJ,MAAI,CAAC,WAAW;AACd,oBAAgB;AAAA,EAClB,WAAW,cAAc;AACvB,oBAAgB;AAAA,EAClB,OAAO;AACL,oBAAgB;AAAA,EAClB;AAGA,QAAM,oBAAoB,QAAQ,WAAW,eAAe,WAAW;AAEvE,MAAI,kBAAkB,UAAU;AAC9B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS;AAAA,MACT;AAAA,MACA,OAAO;AAAA,IAAA;AAAA,EAEX;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS;AAAA,IACT;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"resume-executor.js","sources":["../../../src/cli/commands/resume-executor.ts"],"sourcesContent":["/**\n * Resume executor for CLI resume command.\n *\n * Handles resuming interrupted search sessions by continuing\n * from where providers left off or retrying failed providers.\n */\nimport { readFile, appendFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { ResumeCommandOptions } from './resume.js';\nimport type { Config } from '../../config/index.js';\nimport type {\n Provider,\n TranslatedQuery,\n SearchState,\n} from '../../providers/base/types.js';\nimport { isProviderError } from '../../providers/base/types.js';\nimport {\n loadSession,\n updateDatabaseStatus,\n updateSessionStatus,\n getResumableProviders,\n} from '../../session/manager.js';\nimport { MultiProviderProgress } from '../utils/progress.js';\nimport { createProviderInstance } from './search-executor.js';\nimport { buildFailureErrorMessage } from './search-utils.js';\n\n/**\n * Result of a resume execution.\n */\nexport interface ResumeExecutionResult {\n success: boolean;\n resumed: number;\n results?: Record<string, { hits: number; retrieved: number; error?: string }>;\n error?: string;\n}\n\n/**\n * Interface for providers that support resuming.\n */\ninterface ResumableProviderInstance extends Provider {\n resumeSearch(state: SearchState): AsyncIterable<import('../../providers/base/types.js').Article>;\n}\n\n/**\n * Check if a provider instance supports resuming.\n */\nfunction isResumable(provider: Provider): provider is ResumableProviderInstance {\n return 'resumeSearch' in provider && typeof (provider as any).resumeSearch === 'function';\n}\n\n/**\n * Execute resume for interrupted sessions.\n */\nexport async function executeResume(\n options: ResumeCommandOptions,\n sessionsDir: string,\n config: Config,\n showProgress = true\n): Promise<ResumeExecutionResult> {\n // Load session\n let session;\n try {\n session = await loadSession(options.sessionId, sessionsDir);\n } catch (error) {\n return {\n success: false,\n resumed: 0,\n error: `Failed to load session: ${error instanceof Error ? error.message : error}`,\n };\n }\n\n // Get resumable providers\n let resumableProviders = getResumableProviders(session);\n\n // Filter by specific providers if requested\n if (options.providers && options.providers.length > 0) {\n resumableProviders = resumableProviders.filter((r) =>\n options.providers!.includes(r.provider)\n );\n }\n\n // Filter to only retry strategies if retryFailed is true\n if (options.retryFailed) {\n resumableProviders = resumableProviders.filter((r) => r.strategy === 'retry');\n }\n\n if (resumableProviders.length === 0) {\n return {\n success: true,\n resumed: 0,\n error: 'No providers to resume',\n };\n }\n\n const results: Record<string, { hits: number; retrieved: number; error?: string }> = {};\n\n // Create progress display if enabled\n let progress: MultiProviderProgress | undefined;\n if (showProgress && process.stdout.isTTY) {\n progress = new MultiProviderProgress(resumableProviders.map((p) => p.provider));\n }\n\n let successCount = 0;\n\n // Resume each provider\n for (const resumable of resumableProviders) {\n const providerName = resumable.provider;\n const dbStatus = session.databases[providerName];\n\n if (!dbStatus) continue;\n\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 progress?.fail(providerName, 'Provider configuration missing');\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 options.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 // Build the translated query from stored query file\n const queryPath = join(sessionsDir, options.sessionId, dbStatus.files.query);\n let nativeQuery: string;\n try {\n nativeQuery = await readFile(queryPath, 'utf-8');\n } catch {\n // If query file doesn't exist, skip this provider\n progress?.fail(providerName, 'Query file not found');\n results[providerName] = { hits: 0, retrieved: 0, error: 'Query file not found' };\n continue;\n }\n\n const translatedQuery: TranslatedQuery = {\n native: nativeQuery.trim(),\n provider: providerName,\n };\n\n // Update database status to in_progress\n await updateDatabaseStatus(\n options.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, options.sessionId, dbStatus.files.results);\n\n let retrievedCount = dbStatus.retrievedCount ?? 0;\n const totalHits = dbStatus.totalHits ?? 0;\n\n progress?.update(providerName, retrievedCount, totalHits || 100, 'in_progress');\n\n // Determine how to resume\n if (resumable.strategy === 'continue' && isResumable(provider)) {\n // Build SearchState for continuing\n const searchState: SearchState = {\n provider: providerName,\n query: translatedQuery,\n totalResults: totalHits,\n retrievedCount,\n lastUpdated: new Date(),\n providerState: dbStatus.pagination\n ? {\n cursor: dbStatus.pagination.cursor,\n pageNumber: dbStatus.pagination.pageNumber,\n }\n : undefined,\n };\n\n // Resume search\n for await (const article of provider.resumeSearch(searchState)) {\n retrievedCount++;\n\n // Write article to JSONL file\n await appendFile(resultsPath, JSON.stringify(article) + '\\n', 'utf-8');\n\n progress?.update(providerName, retrievedCount, totalHits || retrievedCount, 'in_progress');\n }\n } else {\n // Fresh start or retry - use regular search\n const searchOptions = {\n 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 progress?.update(providerName, retrievedCount, totalHits || retrievedCount * 2, 'in_progress');\n }\n }\n\n // Mark as completed\n progress?.complete(providerName);\n\n // Update database status\n await updateDatabaseStatus(\n options.sessionId,\n providerName,\n {\n status: 'completed',\n completedAt: new Date().toISOString(),\n totalHits: totalHits || retrievedCount,\n retrievedCount,\n },\n sessionsDir\n );\n\n results[providerName] = { hits: totalHits || retrievedCount, retrieved: retrievedCount };\n successCount++;\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 options.sessionId,\n providerName,\n {\n status: 'failed',\n completedAt: new Date().toISOString(),\n error: {\n code: 'RESUME_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 = resumableProviders.some((p) => {\n const r = results[p.provider];\n return r && r.error !== undefined;\n });\n const anySucceeded = resumableProviders.some((p) => {\n const r = results[p.provider];\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(options.sessionId, sessionStatus, sessionsDir);\n\n if (sessionStatus === 'failed') {\n return {\n success: false,\n resumed: successCount,\n results,\n error: buildFailureErrorMessage(results),\n };\n }\n\n return {\n success: true,\n resumed: successCount,\n results,\n };\n}\n"],"names":[],"mappings":"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"}
@@ -11,6 +11,8 @@ export interface SearchExecutionResult {
11
11
  results?: Record<string, {
12
12
  hits: number;
13
13
  retrieved: number;
14
+ error?: string;
15
+ warnings?: string[];
14
16
  }>;
15
17
  error?: string;
16
18
  autoRegisterResult?: RegistrationRecord;
@@ -18,7 +20,7 @@ export interface SearchExecutionResult {
18
20
  /**
19
21
  * Create a provider instance for the given provider name.
20
22
  */
21
- export declare function createProviderInstance(name: ProviderName, config: Config): Provider;
23
+ export declare function createProviderInstance(name: ProviderName, config: Config): Provider | null;
22
24
  /**
23
25
  * Execute a search across multiple providers.
24
26
  */
@@ -1 +1 @@
1
- {"version":3,"file":"search-executor.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/search-executor.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAEV,QAAQ,EACR,YAAY,EAEb,MAAM,+BAA+B,CAAC;AAqBvC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAOD;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,MAAM,GACb,QAAQ,CAgDV;AAyCD;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,EAC7B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,UAAO,GAClB,OAAO,CAAC,qBAAqB,CAAC,CA8QhC"}
1
+ {"version":3,"file":"search-executor.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/search-executor.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAEV,QAAQ,EACR,YAAY,EAEb,MAAM,+BAA+B,CAAC;AAwBvC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACnG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAOD;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,MAAM,GACb,QAAQ,GAAG,IAAI,CA2DjB;AAyCD;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,EAC7B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,UAAO,GAClB,OAAO,CAAC,qBAAqB,CAAC,CA2ShC"}
@@ -1,6 +1,7 @@
1
1
  import { readFile, writeFile, appendFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { createHash } from "node:crypto";
4
+ import { isProviderError } from "../../providers/base/types.js";
4
5
  import { parseQueryString } from "../../query/parser.js";
5
6
  import "../../query/validator.js";
6
7
  import { createSession, updateDatabaseStatus, updateSessionStatus } from "../../session/manager.js";
@@ -15,6 +16,8 @@ import { translateQuery as translateQuery$1 } from "../../providers/arxiv/transl
15
16
  import { translateQuery } from "../../providers/scopus/translator.js";
16
17
  import { stringify } from "yaml";
17
18
  import { registerArticles, saveRegistrationRecord } from "../../integration/register.js";
19
+ import { buildFailureErrorMessage } from "./search-utils.js";
20
+ import { getConfigDir } from "../../config/paths.js";
18
21
  import { checkRefAvailable } from "../../integration/ref-cli.js";
19
22
  const IMPLEMENTED_PROVIDERS = ["pubmed", "eric", "arxiv", "scopus"];
20
23
  function createProviderInstance(name, config) {
@@ -22,8 +25,11 @@ function createProviderInstance(name, config) {
22
25
  switch (name) {
23
26
  case "pubmed": {
24
27
  if (!providerConfig.email) {
28
+ const configPath = getConfigDir();
25
29
  console.warn(
26
- "Warning: No email configured for PubMed. Set providers.pubmed.email in config."
30
+ `Warning: No email configured for PubMed.
31
+ → Edit ${configPath}/config.toml and set providers.pubmed.email
32
+ → Or run: search-hub config providers.pubmed.email "your@email.com"`
27
33
  );
28
34
  }
29
35
  const pubmedOpts = {
@@ -50,8 +56,16 @@ function createProviderInstance(name, config) {
50
56
  retries: providerConfig.retries
51
57
  });
52
58
  case "scopus": {
59
+ if (!providerConfig.api_key) {
60
+ console.warn(
61
+ `Warning: Scopus requires an API key. Set providers.scopus.api_key in config.
62
+ → Get an API key at https://dev.elsevier.com/
63
+ → Run: search-hub config providers.scopus.api_key "your-key"`
64
+ );
65
+ return null;
66
+ }
53
67
  const scopusOpts = {
54
- apiKey: providerConfig.api_key ?? "",
68
+ apiKey: providerConfig.api_key,
55
69
  rateLimit: providerConfig.rate_limit,
56
70
  timeout: providerConfig.timeout,
57
71
  retries: providerConfig.retries
@@ -165,6 +179,25 @@ async function executeSearch(options, sessionsDir, config, showProgress = true)
165
179
  for (const providerName of providers) {
166
180
  try {
167
181
  const provider = createProviderInstance(providerName, config);
182
+ if (provider === null) {
183
+ const configError = `${providerName}: provider configuration incomplete. See warning above for details.`;
184
+ results[providerName] = { hits: 0, retrieved: 0, error: configError };
185
+ await updateDatabaseStatus(
186
+ sessionId,
187
+ providerName,
188
+ {
189
+ status: "failed",
190
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
191
+ error: {
192
+ code: "CONFIG_ERROR",
193
+ message: configError,
194
+ retryable: false
195
+ }
196
+ },
197
+ sessionsDir
198
+ );
199
+ continue;
200
+ }
168
201
  let translatedQuery;
169
202
  if (options.directQuery && options.providers?.length === 1) {
170
203
  translatedQuery = {
@@ -217,9 +250,10 @@ async function executeSearch(options, sessionsDir, config, showProgress = true)
217
250
  },
218
251
  sessionsDir
219
252
  );
220
- results[providerName] = { hits: totalHits, retrieved: retrievedCount };
253
+ const providerWarnings = "getWarnings" in provider && typeof provider.getWarnings === "function" ? provider.getWarnings() : void 0;
254
+ results[providerName] = { hits: totalHits, retrieved: retrievedCount, ...providerWarnings && providerWarnings.length > 0 && { warnings: providerWarnings } };
221
255
  } catch (error) {
222
- const errorMessage = error instanceof Error ? error.message : String(error);
256
+ const errorMessage = error instanceof Error ? error.message : isProviderError(error) ? error.message : String(error);
223
257
  progress?.fail(providerName, errorMessage);
224
258
  await updateDatabaseStatus(
225
259
  sessionId,
@@ -235,13 +269,13 @@ async function executeSearch(options, sessionsDir, config, showProgress = true)
235
269
  },
236
270
  sessionsDir
237
271
  );
238
- results[providerName] = { hits: 0, retrieved: 0 };
272
+ results[providerName] = { hits: 0, retrieved: 0, error: errorMessage };
239
273
  }
240
274
  }
241
275
  progress?.stop();
242
276
  const anyFailed = providers.some((p) => {
243
277
  const r = results[p];
244
- return r && r.retrieved === 0 && r.hits === 0;
278
+ return r && r.error !== void 0;
245
279
  });
246
280
  const anySucceeded = providers.some((p) => {
247
281
  const r = results[p];
@@ -261,7 +295,7 @@ async function executeSearch(options, sessionsDir, config, showProgress = true)
261
295
  success: false,
262
296
  sessionId,
263
297
  results,
264
- error: "All providers failed"
298
+ error: buildFailureErrorMessage(results)
265
299
  };
266
300
  }
267
301
  let autoRegisterResult;
@@ -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 } 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 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 type { RegistrationRecord } from '../../integration/types.js';\nimport { checkRefAvailable } from '../../integration/ref-cli.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 }>;\n error?: string;\n autoRegisterResult?: RegistrationRecord;\n}\n\n/**\n * Available providers that are implemented.\n */\nconst IMPLEMENTED_PROVIDERS: ProviderName[] = ['pubmed', 'eric', 'arxiv', 'scopus'];\n\n/**\n * Create a provider instance for the given provider name.\n */\nexport function createProviderInstance(\n name: ProviderName,\n config: Config\n): Provider {\n const providerConfig = config.providers[name];\n\n switch (name) {\n case 'pubmed': {\n if (!providerConfig.email) {\n console.warn(\n 'Warning: No email configured for PubMed. Set providers.pubmed.email in config.'\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 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 error: `Failed to parse query file: ${error instanceof Error ? error.message : error}`,\n };\n }\n } else {\n return {\n success: false,\n error: 'Either queryFile or directQuery with provider is required',\n };\n }\n\n // Determine which providers to use\n const providers = getEnabledProviders(config, options.providers);\n\n if (providers.length === 0) {\n return {\n success: false,\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 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 }> = {};\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 // 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 // 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 },\n },\n sessionsDir\n );\n\n results[providerName] = { hits: totalHits, retrieved: retrievedCount };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 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 };\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.retrieved === 0 && r.hits === 0;\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 results,\n error: 'All providers failed',\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 };\n\n if (autoRegisterResult) {\n result.autoRegisterResult = autoRegisterResult;\n }\n\n return result;\n}\n\n/**\n * Load all articles from a session's results files.\n */\nasync function loadArticlesFromSession(\n sessionsDir: string,\n sessionId: string,\n providers: ProviderName[]\n): Promise<Article[]> {\n const articles: Article[] = [];\n\n for (const provider of providers) {\n const resultsPath = join(sessionsDir, sessionId, `${provider}_results.jsonl`);\n try {\n const content = await readFile(resultsPath, 'utf-8');\n const lines = content.trim().split('\\n').filter((line) => line.length > 0);\n for (const line of lines) {\n try {\n const article = JSON.parse(line) as Article;\n articles.push(article);\n } catch {\n // Skip invalid JSON lines\n }\n }\n } catch {\n // Skip if file doesn't exist\n }\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 } 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 } from './search-utils.js';\nimport { getConfigDir } from '../../config/paths.js';\nimport type { RegistrationRecord } from '../../integration/types.js';\nimport { checkRefAvailable } from '../../integration/ref-cli.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}\n\n/**\n * Available providers that are implemented.\n */\nconst IMPLEMENTED_PROVIDERS: ProviderName[] = ['pubmed', 'eric', 'arxiv', 'scopus'];\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 error: `Failed to parse query file: ${error instanceof Error ? error.message : error}`,\n };\n }\n } else {\n return {\n success: false,\n error: 'Either queryFile or directQuery with provider is required',\n };\n }\n\n // Determine which providers to use\n const providers = getEnabledProviders(config, options.providers);\n\n if (providers.length === 0) {\n return {\n success: false,\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 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 // 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 },\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 results,\n error: buildFailureErrorMessage(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 };\n\n if (autoRegisterResult) {\n result.autoRegisterResult = autoRegisterResult;\n }\n\n return result;\n}\n\n/**\n * Load all articles from a session's results files.\n */\nasync function loadArticlesFromSession(\n sessionsDir: string,\n sessionId: string,\n providers: ProviderName[]\n): Promise<Article[]> {\n const articles: Article[] = [];\n\n for (const provider of providers) {\n const resultsPath = join(sessionsDir, sessionId, `${provider}_results.jsonl`);\n try {\n const content = await readFile(resultsPath, 'utf-8');\n const lines = content.trim().split('\\n').filter((line) => line.length > 0);\n for (const line of lines) {\n try {\n const article = JSON.parse(line) as Article;\n articles.push(article);\n } catch {\n // Skip invalid JSON lines\n }\n }\n } catch {\n // Skip if file doesn't exist\n }\n }\n\n return articles;\n}\n"],"names":["translatePubmed","translateEric","translateArxiv","translateScopus","stringifyYaml"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAyDA,MAAM,wBAAwC,CAAC,UAAU,QAAQ,SAAS,QAAQ;AAK3E,SAAS,uBACd,MACA,QACiB;AACjB,QAAM,iBAAiB,OAAO,UAAU,IAAI;AAE5C,UAAQ,MAAA;AAAA,IACN,KAAK,UAAU;AACb,UAAI,CAAC,eAAe,OAAO;AACzB,cAAM,aAAa,aAAA;AACnB,gBAAQ;AAAA,UACN;AAAA,WACY,UAAU;AAAA;AAAA,QAAA;AAAA,MAG1B;AACA,YAAM,aAA2B;AAAA,QAC/B,OAAO,eAAe,SAAS;AAAA,QAC/B,WAAW,eAAe;AAAA,QAC1B,SAAS,eAAe;AAAA,QACxB,SAAS,eAAe;AAAA,MAAA;AAE1B,UAAI,eAAe,SAAS;AAC1B,mBAAW,SAAS,eAAe;AAAA,MACrC;AACA,aAAO,IAAI,eAAe,UAAU;AAAA,IACtC;AAAA,IACA,KAAK;AACH,aAAO,IAAI,aAAa;AAAA,QACtB,WAAW,eAAe;AAAA,QAC1B,SAAS,eAAe;AAAA,QACxB,SAAS,eAAe;AAAA,MAAA,CACzB;AAAA,IACH,KAAK;AACH,aAAO,IAAI,cAAc;AAAA,QACvB,WAAW,eAAe;AAAA,QAC1B,SAAS,eAAe;AAAA,QACxB,SAAS,eAAe;AAAA,MAAA,CACzB;AAAA,IACH,KAAK,UAAU;AACb,UAAI,CAAC,eAAe,SAAS;AAC3B,gBAAQ;AAAA,UACN;AAAA;AAAA;AAAA,QAAA;AAIF,eAAO;AAAA,MACT;AACA,YAAM,aAA2B;AAAA,QAC/B,QAAQ,eAAe;AAAA,QACvB,WAAW,eAAe;AAAA,QAC1B,SAAS,eAAe;AAAA,QACxB,SAAS,eAAe;AAAA,MAAA;AAE1B,UAAI,eAAe,YAAY;AAC7B,mBAAW,YAAY,eAAe;AAAA,MACxC;AACA,aAAO,IAAI,eAAe,UAAU;AAAA,IACtC;AAAA,IACA;AACE,YAAM,IAAI,MAAM,aAAa,IAAI,sBAAsB;AAAA,EAAA;AAE7D;AAKA,SAAS,0BACP,KACA,UACiB;AACjB,UAAQ,UAAA;AAAA,IACN,KAAK;AACH,aAAOA,iBAAgB,GAAG;AAAA,IAC5B,KAAK;AACH,aAAOC,iBAAc,GAAG;AAAA,IAC1B,KAAK;AACH,aAAOC,iBAAe,GAAG;AAAA,IAC3B,KAAK;AACH,aAAOC,eAAgB,GAAG;AAAA,IAC5B;AACE,YAAM,IAAI,MAAM,+BAA+B,QAAQ,GAAG;AAAA,EAAA;AAEhE;AAKA,SAAS,oBACP,QACA,oBACgB;AAChB,QAAM,kBAAkB,sBAAsB;AAAA,IAC5C,CAAC,SAAS,OAAO,UAAU,IAAI,EAAE;AAAA,EAAA;AAGnC,MAAI,sBAAsB,mBAAmB,SAAS,GAAG;AACvD,WAAO,mBAAmB,OAAO,CAAC,MAAM,gBAAgB,SAAS,CAAC,CAAC;AAAA,EACrE;AAEA,SAAO;AACT;AAKA,eAAsB,cACpB,SACA,aACA,QACA,eAAe,MACiB;AAChC,MAAI;AACJ,MAAI;AACJ,MAAI;AAGJ,MAAI,QAAQ,eAAe,QAAQ,aAAa,QAAQ,UAAU,WAAW,GAAG;AAC9E,gBAAY;AAGZ,UAAM;AAAA,MACJ,MAAM,QAAQ,eAAe;AAAA,MAC7B,QAAQ;AAAA,QACN;AAAA,UACE,OAAO;AAAA,UACP,OAAO,EAAE,UAAU,CAAC,QAAQ,WAAW,EAAA;AAAA,UACvC,UAAU;AAAA,QAAA;AAAA,MACZ;AAAA,MAEF,SAAS,CAAA;AAAA,MACT,WAAW,CAAA;AAAA,IAAC;AAId,mBAAeC,UAAc;AAAA,MAC3B,MAAM,IAAI;AAAA,MACV,QAAQ,IAAI;AAAA,MACZ,SAAS,IAAI;AAAA,IAAA,CACd;AAAA,EACH,WAAW,QAAQ,WAAW;AAE5B,QAAI;AACF,qBAAe,MAAM,SAAS,QAAQ,WAAW,OAAO;AACxD,YAAM,iBAAiB,YAAY;AACnC,kBAAY,QAAQ;AAAA,IACtB,SAAS,OAAO;AACd,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO,+BAA+B,iBAAiB,QAAQ,MAAM,UAAU,KAAK;AAAA,MAAA;AAAA,IAExF;AAAA,EACF,OAAO;AACL,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AAGA,QAAM,YAAY,oBAAoB,QAAQ,QAAQ,SAAS;AAE/D,MAAI,UAAU,WAAW,GAAG;AAC1B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AAGA,QAAM,YAAY,WAAW,QAAQ,EAAE,OAAO,YAAY,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,CAAC;AAGpF,MAAI;AACJ,MAAI;AACF,UAAM,cAAmD;AAAA,MACvD,MAAM,QAAQ,eAAe,IAAI;AAAA,MACjC;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS;AAAA,MACT;AAAA,IAAA;AAEF,QAAI,IAAI,aAAa;AACnB,kBAAY,cAAc,IAAI;AAAA,IAChC;AACA,cAAU,MAAM,cAAc,WAAW;AAAA,EAC3C,SAAS,OAAO;AACd,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,6BAA6B,iBAAiB,QAAQ,MAAM,UAAU,KAAK;AAAA,IAAA;AAAA,EAEtF;AAEA,QAAM,YAAY,QAAQ;AAC1B,QAAM,UAAoG,CAAA;AAG1G,MAAI;AACJ,MAAI,gBAAgB,QAAQ,OAAO,OAAO;AACxC,eAAW,IAAI,sBAAsB,SAAS;AAAA,EAChD;AAGA,aAAW,gBAAgB,WAAW;AACpC,QAAI;AAEF,YAAM,WAAW,uBAAuB,cAAc,MAAM;AAG5D,UAAI,aAAa,MAAM;AACrB,cAAM,cAAc,GAAG,YAAY;AACnC,gBAAQ,YAAY,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,YAAA;AACxD,cAAM;AAAA,UACJ;AAAA,UACA;AAAA,UACA;AAAA,YACE,QAAQ;AAAA,YACR,cAAa,oBAAI,KAAA,GAAO,YAAA;AAAA,YACxB,OAAO;AAAA,cACL,MAAM;AAAA,cACN,SAAS;AAAA,cACT,WAAW;AAAA,YAAA;AAAA,UACb;AAAA,UAEF;AAAA,QAAA;AAEF;AAAA,MACF;AAGA,UAAI;AACJ,UAAI,QAAQ,eAAe,QAAQ,WAAW,WAAW,GAAG;AAE1D,0BAAkB;AAAA,UAChB,QAAQ,QAAQ;AAAA,UAChB,UAAU;AAAA,QAAA;AAAA,MAEd,OAAO;AACL,0BAAkB,0BAA0B,KAAK,YAAY;AAAA,MAC/D;AAGA,YAAM,YAAY,KAAK,aAAa,WAAW,GAAG,YAAY,YAAY;AAC1E,YAAM,UAAU,WAAW,gBAAgB,QAAQ,OAAO;AAG1D,YAAM;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,YAAW,oBAAI,KAAA,GAAO,YAAA;AAAA,QAAY;AAAA,QAEpC;AAAA,MAAA;AAIF,YAAM,cAAc,KAAK,aAAa,WAAW,GAAG,YAAY,gBAAgB;AAGhF,UAAI,iBAAiB;AACrB,UAAI,YAAY;AAEhB,gBAAU,OAAO,cAAc,GAAG,GAAG,aAAa;AAElD,YAAM,gBAAgB;AAAA,QACpB,YAAY,QAAQ,cAAc,OAAO,UAAU,YAAY,EAAE;AAAA,MAAA;AAGnE,uBAAiB,WAAW,SAAS,OAAO,iBAAiB,aAAa,GAAG;AAC3E;AAGA,cAAM,WAAW,aAAa,KAAK,UAAU,OAAO,IAAI,MAAM,OAAO;AAGrE,YAAI,cAAc,GAAG;AAEnB,sBAAY,KAAK,IAAI,iBAAiB,IAAI,GAAG;AAAA,QAC/C;AACA,kBAAU,OAAO,cAAc,gBAAgB,WAAW,aAAa;AAAA,MACzE;AAGA,kBAAY;AAGZ,gBAAU,SAAS,YAAY;AAG/B,YAAM;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,cAAa,oBAAI,KAAA,GAAO,YAAA;AAAA,UACxB;AAAA,UACA;AAAA,UACA,OAAO;AAAA,YACL,OAAO,GAAG,YAAY;AAAA,YACtB,SAAS,GAAG,YAAY;AAAA,UAAA;AAAA,QAC1B;AAAA,QAEF;AAAA,MAAA;AAIF,YAAM,mBAAmB,iBAAiB,YAAY,OAAQ,SAAiB,gBAAgB,aAC1F,SAAiB,YAAA,IAClB;AACJ,cAAQ,YAAY,IAAI,EAAE,MAAM,WAAW,WAAW,gBAAgB,GAAI,oBAAoB,iBAAiB,SAAS,KAAK,EAAE,UAAU,mBAAiB;AAAA,IAC5J,SAAS,OAAO;AACd,YAAM,eAAe,iBAAiB,QAChC,MAAM,UACN,gBAAgB,KAAK,IACnB,MAAM,UACN,OAAO,KAAK;AAEpB,gBAAU,KAAK,cAAc,YAAY;AAGzC,YAAM;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,cAAa,oBAAI,KAAA,GAAO,YAAA;AAAA,UACxB,OAAO;AAAA,YACL,MAAM;AAAA,YACN,SAAS;AAAA,YACT,WAAW;AAAA,UAAA;AAAA,QACb;AAAA,QAEF;AAAA,MAAA;AAGF,cAAQ,YAAY,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,aAAA;AAAA,IAC1D;AAAA,EACF;AAGA,YAAU,KAAA;AAGV,QAAM,YAAY,UAAU,KAAK,CAAC,MAAM;AACtC,UAAM,IAAI,QAAQ,CAAC;AACnB,WAAO,KAAK,EAAE,UAAU;AAAA,EAC1B,CAAC;AACD,QAAM,eAAe,UAAU,KAAK,CAAC,MAAM;AACzC,UAAM,IAAI,QAAQ,CAAC;AACnB,WAAO,KAAK,EAAE,YAAY;AAAA,EAC5B,CAAC;AAED,MAAI;AACJ,MAAI,CAAC,WAAW;AACd,oBAAgB;AAAA,EAClB,WAAW,cAAc;AACvB,oBAAgB;AAAA,EAClB,OAAO;AACL,oBAAgB;AAAA,EAClB;AAGA,QAAM,oBAAoB,WAAW,eAAe,WAAW;AAE/D,MAAI,kBAAkB,UAAU;AAC9B,WAAO;AAAA,MACL,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA,OAAO,yBAAyB,OAAO;AAAA,IAAA;AAAA,EAE3C;AAGA,MAAI;AACJ,MACE,OAAO,YAAY,kBAAkB,WACrC,OAAO,YAAY,kBAAkB,eACrC;AACA,UAAM,eAAe,MAAM,kBAAA;AAC3B,QAAI,cAAc;AAEhB,YAAM,cAAc,MAAM,wBAAwB,aAAa,WAAW,SAAS;AAEnF,UAAI,YAAY,SAAS,GAAG;AAC1B,6BAAqB,MAAM,iBAAiB,aAAa;AAAA,UACvD;AAAA,UACA,YAAY,KAAK,aAAa,SAAS;AAAA,UACvC,eAAe,OAAO,YAAY,kBAAkB;AAAA,QAAA,CACrD;AAGD,cAAM,uBAAuB,KAAK,aAAa,SAAS,GAAG,kBAAkB;AAAA,MAC/E;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAgC;AAAA,IACpC,SAAS;AAAA,IACT;AAAA,IACA;AAAA,EAAA;AAGF,MAAI,oBAAoB;AACtB,WAAO,qBAAqB;AAAA,EAC9B;AAEA,SAAO;AACT;AAKA,eAAe,wBACb,aACA,WACA,WACoB;AACpB,QAAM,WAAsB,CAAA;AAE5B,aAAW,YAAY,WAAW;AAChC,UAAM,cAAc,KAAK,aAAa,WAAW,GAAG,QAAQ,gBAAgB;AAC5E,QAAI;AACF,YAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,YAAM,QAAQ,QAAQ,KAAA,EAAO,MAAM,IAAI,EAAE,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC;AACzE,iBAAW,QAAQ,OAAO;AACxB,YAAI;AACF,gBAAM,UAAU,KAAK,MAAM,IAAI;AAC/B,mBAAS,KAAK,OAAO;AAAA,QACvB,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO;AACT;"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared utilities for search and resume executors.
3
+ */
4
+ /**
5
+ * Build a detailed error message listing per-provider failures.
6
+ */
7
+ export declare function buildFailureErrorMessage(results: Record<string, {
8
+ hits: number;
9
+ retrieved: number;
10
+ error?: string;
11
+ }>): string;
12
+ /**
13
+ * Format verbose per-provider details for CLI output.
14
+ */
15
+ export declare function formatVerboseProviderDetails(results: Record<string, {
16
+ hits: number;
17
+ retrieved: number;
18
+ error?: string;
19
+ warnings?: string[];
20
+ }>): string;
21
+ //# sourceMappingURL=search-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-utils.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/search-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAC3E,MAAM,CAeR;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,GAChG,MAAM,CAeR"}
@@ -0,0 +1,29 @@
1
+ function buildFailureErrorMessage(results) {
2
+ const errorLines = Object.entries(results).filter(([, r]) => r.error).map(([provider, r]) => ` ${provider}: ${r.error}`);
3
+ const suggestedActions = "\n\nSuggested actions:\n → Run with --dry-run to inspect translated queries\n → Check provider configuration: search-hub config\n → Use --db <provider> to test a single provider";
4
+ if (errorLines.length === 0) {
5
+ return "All providers failed" + suggestedActions;
6
+ }
7
+ return "All providers failed:\n" + errorLines.join("\n") + suggestedActions;
8
+ }
9
+ function formatVerboseProviderDetails(results) {
10
+ const lines = ["\nPer-provider details:"];
11
+ for (const [provider, stats] of Object.entries(results)) {
12
+ if (stats.error) {
13
+ lines.push(` ${provider}: FAILED - ${stats.error}`);
14
+ } else {
15
+ lines.push(` ${provider}: ${stats.retrieved} results`);
16
+ }
17
+ if (stats.warnings && stats.warnings.length > 0) {
18
+ for (const w of stats.warnings) {
19
+ lines.push(` warning: ${w}`);
20
+ }
21
+ }
22
+ }
23
+ return lines.join("\n");
24
+ }
25
+ export {
26
+ buildFailureErrorMessage,
27
+ formatVerboseProviderDetails
28
+ };
29
+ //# sourceMappingURL=search-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-utils.js","sources":["../../../src/cli/commands/search-utils.ts"],"sourcesContent":["/**\n * Shared utilities for search and resume executors.\n */\n\n/**\n * Build a detailed error message listing per-provider failures.\n */\nexport function buildFailureErrorMessage(\n results: Record<string, { hits: number; retrieved: number; error?: string }>\n): string {\n const errorLines = Object.entries(results)\n .filter(([, r]) => r.error)\n .map(([provider, r]) => ` ${provider}: ${r.error}`);\n\n const suggestedActions = '\\n\\nSuggested actions:\\n' +\n ' → Run with --dry-run to inspect translated queries\\n' +\n ' → Check provider configuration: search-hub config\\n' +\n ' → Use --db <provider> to test a single provider';\n\n if (errorLines.length === 0) {\n return 'All providers failed' + suggestedActions;\n }\n\n return 'All providers failed:\\n' + errorLines.join('\\n') + suggestedActions;\n}\n\n/**\n * Format verbose per-provider details for CLI output.\n */\nexport function formatVerboseProviderDetails(\n results: Record<string, { hits: number; retrieved: number; error?: string; warnings?: string[] }>\n): string {\n const lines: string[] = ['\\nPer-provider details:'];\n for (const [provider, stats] of Object.entries(results)) {\n if (stats.error) {\n lines.push(` ${provider}: FAILED - ${stats.error}`);\n } else {\n lines.push(` ${provider}: ${stats.retrieved} results`);\n }\n if (stats.warnings && stats.warnings.length > 0) {\n for (const w of stats.warnings) {\n lines.push(` warning: ${w}`);\n }\n }\n }\n return lines.join('\\n');\n}\n"],"names":[],"mappings":"AAOO,SAAS,yBACd,SACQ;AACR,QAAM,aAAa,OAAO,QAAQ,OAAO,EACtC,OAAO,CAAC,CAAA,EAAG,CAAC,MAAM,EAAE,KAAK,EACzB,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,KAAK,QAAQ,KAAK,EAAE,KAAK,EAAE;AAErD,QAAM,mBAAmB;AAKzB,MAAI,WAAW,WAAW,GAAG;AAC3B,WAAO,yBAAyB;AAAA,EAClC;AAEA,SAAO,4BAA4B,WAAW,KAAK,IAAI,IAAI;AAC7D;AAKO,SAAS,6BACd,SACQ;AACR,QAAM,QAAkB,CAAC,yBAAyB;AAClD,aAAW,CAAC,UAAU,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AACvD,QAAI,MAAM,OAAO;AACf,YAAM,KAAK,KAAK,QAAQ,cAAc,MAAM,KAAK,EAAE;AAAA,IACrD,OAAO;AACL,YAAM,KAAK,KAAK,QAAQ,KAAK,MAAM,SAAS,UAAU;AAAA,IACxD;AACA,QAAI,MAAM,YAAY,MAAM,SAAS,SAAS,GAAG;AAC/C,iBAAW,KAAK,MAAM,UAAU;AAC9B,cAAM,KAAK,gBAAgB,CAAC,EAAE;AAAA,MAChC;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
@@ -1,4 +1,5 @@
1
1
  import { ProviderName } from '../../session/types.js';
2
+ import { Config } from '../../config/index.js';
2
3
  export interface SearchCommandOptions {
3
4
  queryFile?: string;
4
5
  directQuery?: string;
@@ -24,7 +25,22 @@ export interface ValidationResult {
24
25
  valid: boolean;
25
26
  error?: string;
26
27
  }
28
+ /**
29
+ * Options for enhanced dry-run output.
30
+ */
31
+ export interface DryRunOutputOptions {
32
+ config?: Config;
33
+ providers?: ProviderName[];
34
+ }
27
35
  export declare function parseSearchOptions(queryFile: string | undefined, options: CommandLineOptions): SearchCommandOptions;
28
36
  export declare function validateSearchInput(options: SearchCommandOptions): ValidationResult;
29
- export declare function formatDryRunOutput(translations: TranslationResult[]): string;
37
+ /**
38
+ * Format provider readiness summary for dry-run output.
39
+ */
40
+ export declare function formatProviderReadiness(providers: ProviderName[], config: Config): string;
41
+ /**
42
+ * Format query diagnostics warnings for dry-run output.
43
+ */
44
+ export declare function formatQueryDiagnostics(translations: TranslationResult[]): string;
45
+ export declare function formatDryRunOutput(translations: TranslationResult[], options?: DryRunOutputOptions): string;
30
46
  //# sourceMappingURL=search.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAG3D,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,OAAO,EAAE,kBAAkB,GAC1B,oBAAoB,CAgCtB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,gBAAgB,CA0BnF;AAED,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAgB5E"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAGpD,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;CAC5B;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,OAAO,EAAE,kBAAkB,GAC1B,oBAAoB,CAgCtB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,gBAAgB,CAuBnF;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,YAAY,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAUzF;AA2BD;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAmBhF;AAED,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,iBAAiB,EAAE,EACjC,OAAO,CAAC,EAAE,mBAAmB,GAC5B,MAAM,CAwBR"}