@ncukondo/search-hub 0.4.1 → 0.4.3

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.
@@ -34,7 +34,13 @@ export interface ListOptions {
34
34
  export interface FormatOptions {
35
35
  json: boolean;
36
36
  }
37
- export declare function listSessionsForDisplay(sessionsDir: string, options: ListOptions): Promise<SessionListItem[]>;
37
+ export interface SessionListResult {
38
+ sessions: SessionListItem[];
39
+ totalCount: number;
40
+ filteredCount: number;
41
+ showingAll: boolean;
42
+ }
43
+ export declare function listSessionsForDisplay(sessionsDir: string, options: ListOptions): Promise<SessionListResult>;
38
44
  export declare function getSessionDetails(sessionId: string, sessionsDir: string): Promise<{
39
45
  success: boolean;
40
46
  session?: SessionDetails;
@@ -50,6 +56,6 @@ export declare function computeDeduplicationStats(sessionId: string, sessionsDir
50
56
  uniqueArticles: number;
51
57
  duplicatesRemoved: number;
52
58
  }>;
53
- export declare function formatSessionList(sessions: SessionListItem[], options: FormatOptions): string;
59
+ export declare function formatSessionList(result: SessionListResult, options: FormatOptions): string;
54
60
  export declare function formatSessionDetails(details: SessionDetails, options: FormatOptions): string;
55
61
  //# sourceMappingURL=status.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/status.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAIxE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,eAAe,EAAE,CAAC,CAc5B;AAED,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAwDzE;AAED,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,SAAS,CAAC,CAAA;CAAE,GACnF,OAAO,CAAC;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBhE;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,eAAe,EAAE,EAC3B,OAAO,EAAE,aAAa,GACrB,MAAM,CAkBR;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,aAAa,GACrB,MAAM,CA0CR"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/status.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAIxE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,iBAAiB,CAAC,CAqB5B;AAED,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAwDzE;AAED,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,SAAS,CAAC,CAAA;CAAE,GACnF,OAAO,CAAC;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBhE;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,aAAa,GACrB,MAAM,CAgCR;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,aAAa,GACrB,MAAM,CA0CR"}
@@ -6,13 +6,19 @@ import { loadResults } from "../../session/results-io.js";
6
6
  async function listSessionsForDisplay(sessionsDir, options) {
7
7
  const summaries = await listSessions(sessionsDir);
8
8
  const filtered = options.all ? summaries : summaries.filter((s) => s.status !== "completed");
9
- return filtered.map((s) => ({
9
+ const sessions = filtered.map((s) => ({
10
10
  id: s.id,
11
11
  name: s.name,
12
12
  status: s.status,
13
13
  createdAt: s.createdAt,
14
14
  progress: `${s.totalRetrieved}/${s.totalHits}`
15
15
  }));
16
+ return {
17
+ sessions,
18
+ totalCount: summaries.length,
19
+ filteredCount: filtered.length,
20
+ showingAll: options.all
21
+ };
16
22
  }
17
23
  async function getSessionDetails(sessionId, sessionsDir) {
18
24
  try {
@@ -81,20 +87,30 @@ async function computeDeduplicationStats(sessionId, sessionsDir, session) {
81
87
  duplicatesRemoved: result.duplicatesRemoved
82
88
  };
83
89
  }
84
- function formatSessionList(sessions, options) {
90
+ function formatSessionList(result, options) {
85
91
  if (options.json) {
86
- return JSON.stringify(sessions, null, 2);
92
+ return JSON.stringify(result.sessions, null, 2);
87
93
  }
88
- if (sessions.length === 0) {
89
- return "No sessions found.";
94
+ if (result.sessions.length === 0) {
95
+ if (result.totalCount === 0) {
96
+ return "No sessions found.";
97
+ }
98
+ const completedCount = result.totalCount - result.filteredCount;
99
+ return `No active sessions. ${completedCount} completed session${completedCount === 1 ? "" : "s"} hidden (use --all to show).`;
90
100
  }
91
101
  const header = `${"ID".padEnd(35)} ${"NAME".padEnd(20)} ${"STATUS".padEnd(15)} ${"PROGRESS".padEnd(12)} CREATED`;
92
102
  const separator = "-".repeat(100);
93
- const rows = sessions.map((s) => {
103
+ const rows = result.sessions.map((s) => {
94
104
  const date = new Date(s.createdAt).toLocaleDateString();
95
105
  return `${s.id.padEnd(35)} ${s.name.padEnd(20)} ${s.status.padEnd(15)} ${s.progress.padEnd(12)} ${date}`;
96
106
  });
97
- return [header, separator, ...rows].join("\n");
107
+ const lines = [header, separator, ...rows];
108
+ if (!result.showingAll && result.totalCount > result.filteredCount) {
109
+ const hiddenCount = result.totalCount - result.filteredCount;
110
+ lines.push("");
111
+ lines.push(`(${hiddenCount} completed session${hiddenCount === 1 ? "" : "s"} hidden, use --all to show)`);
112
+ }
113
+ return lines.join("\n");
98
114
  }
99
115
  function formatSessionDetails(details, options) {
100
116
  if (options.json) {
@@ -1 +1 @@
1
- {"version":3,"file":"status.js","sources":["../../../src/cli/commands/status.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { listSessions, loadSession } from '../../session/manager.js';\nimport { deduplicateArticles } from './export.js';\nimport { loadNotes, formatNotesList, type NoteEntry } from './notes.js';\nimport { loadResults } from '../../session/results-io.js';\nimport type { ProviderName } from '../../providers/base/types.js';\n\nexport interface SessionListItem {\n id: string;\n name: string;\n status: string;\n createdAt: string;\n progress: string;\n}\n\nexport interface DatabaseDetails {\n provider: string;\n status: string;\n totalHits: number;\n retrievedCount: number;\n error?: string;\n}\n\nexport interface SessionDetails {\n id: string;\n name: string;\n description?: string;\n status: string;\n createdAt: string;\n updatedAt: string;\n queryFile: string;\n totalHits: number;\n totalRetrieved: number;\n databases: DatabaseDetails[];\n uniqueArticles?: number;\n duplicatesRemoved?: number;\n notes?: NoteEntry[];\n}\n\nexport interface ListOptions {\n all: boolean;\n}\n\nexport interface FormatOptions {\n json: boolean;\n}\n\nexport async function listSessionsForDisplay(\n sessionsDir: string,\n options: ListOptions\n): Promise<SessionListItem[]> {\n const summaries = await listSessions(sessionsDir);\n\n const filtered = options.all\n ? summaries\n : summaries.filter((s) => s.status !== 'completed');\n\n return filtered.map((s) => ({\n id: s.id,\n name: s.name,\n status: s.status,\n createdAt: s.createdAt,\n progress: `${s.totalRetrieved}/${s.totalHits}`,\n }));\n}\n\nexport async function getSessionDetails(\n sessionId: string,\n sessionsDir: string\n): Promise<{ success: boolean; session?: SessionDetails; error?: string }> {\n try {\n const session = await loadSession(sessionId, sessionsDir);\n\n const databases: DatabaseDetails[] = [];\n for (const [provider, dbStatus] of Object.entries(session.databases)) {\n if (!dbStatus) continue;\n const dbDetail: DatabaseDetails = {\n provider,\n status: dbStatus.status,\n totalHits: dbStatus.totalHits ?? 0,\n retrievedCount: dbStatus.retrievedCount ?? 0,\n };\n if (dbStatus.error?.message) {\n dbDetail.error = dbStatus.error.message;\n }\n databases.push(dbDetail);\n }\n\n const sessionDetails: SessionDetails = {\n id: session.id,\n name: session.name,\n status: session.summary.status,\n createdAt: session.createdAt,\n updatedAt: session.updatedAt,\n queryFile: session.query.file,\n totalHits: session.summary.totalHits,\n totalRetrieved: session.summary.totalRetrieved,\n databases,\n };\n if (session.description) {\n sessionDetails.description = session.description;\n }\n\n // Load notes (optional - don't fail if notes can't be loaded)\n try {\n const sessionDir = join(sessionsDir, sessionId);\n const notes = await loadNotes(sessionDir);\n if (notes.length > 0) {\n sessionDetails.notes = notes;\n }\n } catch {\n // Notes are optional - ignore errors\n }\n\n return {\n success: true,\n session: sessionDetails,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n return {\n success: false,\n error: message,\n };\n }\n}\n\nexport async function computeDeduplicationStats(\n sessionId: string,\n sessionsDir: string,\n session: { databases: Record<string, { files?: { results?: string } } | undefined> }\n): Promise<{ uniqueArticles: number; duplicatesRemoved: number }> {\n const articles: import('../../providers/base/types.js').Article[] = [];\n const sessionDir = join(sessionsDir, sessionId);\n\n for (const [providerName] of Object.entries(session.databases)) {\n const providerArticles = await loadResults(sessionDir, providerName as ProviderName);\n articles.push(...providerArticles);\n }\n\n if (articles.length === 0) {\n return { uniqueArticles: 0, duplicatesRemoved: 0 };\n }\n\n const result = deduplicateArticles(articles);\n return {\n uniqueArticles: result.articles.length,\n duplicatesRemoved: result.duplicatesRemoved,\n };\n}\n\nexport function formatSessionList(\n sessions: SessionListItem[],\n options: FormatOptions\n): string {\n if (options.json) {\n return JSON.stringify(sessions, null, 2);\n }\n\n if (sessions.length === 0) {\n return 'No sessions found.';\n }\n\n const header = `${'ID'.padEnd(35)} ${'NAME'.padEnd(20)} ${'STATUS'.padEnd(15)} ${'PROGRESS'.padEnd(12)} CREATED`;\n const separator = '-'.repeat(100);\n\n const rows = sessions.map((s) => {\n const date = new Date(s.createdAt).toLocaleDateString();\n return `${s.id.padEnd(35)} ${s.name.padEnd(20)} ${s.status.padEnd(15)} ${s.progress.padEnd(12)} ${date}`;\n });\n\n return [header, separator, ...rows].join('\\n');\n}\n\nexport function formatSessionDetails(\n details: SessionDetails,\n options: FormatOptions\n): string {\n if (options.json) {\n return JSON.stringify(details, null, 2);\n }\n\n const lines: string[] = [];\n\n lines.push(`Session: ${details.name}`);\n lines.push(`ID: ${details.id}`);\n if (details.description) {\n lines.push(`Description: ${details.description}`);\n }\n lines.push(`Status: ${details.status}`);\n lines.push(`Query File: ${details.queryFile}`);\n lines.push(`Created: ${new Date(details.createdAt).toLocaleString()}`);\n lines.push(`Updated: ${new Date(details.updatedAt).toLocaleString()}`);\n lines.push('');\n if (details.duplicatesRemoved !== undefined && details.duplicatesRemoved > 0) {\n lines.push(`Total: ${details.totalRetrieved} raw / ${details.uniqueArticles} unique (${details.duplicatesRemoved} duplicates)`);\n } else {\n lines.push(`Total: ${details.totalRetrieved}/${details.totalHits} results`);\n }\n lines.push('');\n lines.push('Databases:');\n\n for (const db of details.databases) {\n const statusIcon = getStatusIcon(db.status);\n let line = ` ${statusIcon} ${db.provider.padEnd(10)} ${db.status.padEnd(12)} ${db.retrievedCount}/${db.totalHits}`;\n if (db.error) {\n line += ` (${db.error})`;\n }\n lines.push(line);\n }\n\n // Display notes if present\n if (details.notes && details.notes.length > 0) {\n lines.push('');\n lines.push('Notes:');\n lines.push(formatNotesList(details.notes));\n }\n\n return lines.join('\\n');\n}\n\nfunction getStatusIcon(status: string): string {\n switch (status) {\n case 'completed':\n return '\\u2713'; // ✓\n case 'failed':\n return '\\u2717'; // ✗\n case 'in_progress':\n return '\\u280B'; // ⠋\n case 'pending':\n return '\\u25FC'; // ◼\n default:\n return ' ';\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"status.js","sources":["../../../src/cli/commands/status.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { listSessions, loadSession } from '../../session/manager.js';\nimport { deduplicateArticles } from './export.js';\nimport { loadNotes, formatNotesList, type NoteEntry } from './notes.js';\nimport { loadResults } from '../../session/results-io.js';\nimport type { ProviderName } from '../../providers/base/types.js';\n\nexport interface SessionListItem {\n id: string;\n name: string;\n status: string;\n createdAt: string;\n progress: string;\n}\n\nexport interface DatabaseDetails {\n provider: string;\n status: string;\n totalHits: number;\n retrievedCount: number;\n error?: string;\n}\n\nexport interface SessionDetails {\n id: string;\n name: string;\n description?: string;\n status: string;\n createdAt: string;\n updatedAt: string;\n queryFile: string;\n totalHits: number;\n totalRetrieved: number;\n databases: DatabaseDetails[];\n uniqueArticles?: number;\n duplicatesRemoved?: number;\n notes?: NoteEntry[];\n}\n\nexport interface ListOptions {\n all: boolean;\n}\n\nexport interface FormatOptions {\n json: boolean;\n}\n\nexport interface SessionListResult {\n sessions: SessionListItem[];\n totalCount: number;\n filteredCount: number;\n showingAll: boolean;\n}\n\nexport async function listSessionsForDisplay(\n sessionsDir: string,\n options: ListOptions\n): Promise<SessionListResult> {\n const summaries = await listSessions(sessionsDir);\n\n const filtered = options.all\n ? summaries\n : summaries.filter((s) => s.status !== 'completed');\n\n const sessions = filtered.map((s) => ({\n id: s.id,\n name: s.name,\n status: s.status,\n createdAt: s.createdAt,\n progress: `${s.totalRetrieved}/${s.totalHits}`,\n }));\n\n return {\n sessions,\n totalCount: summaries.length,\n filteredCount: filtered.length,\n showingAll: options.all,\n };\n}\n\nexport async function getSessionDetails(\n sessionId: string,\n sessionsDir: string\n): Promise<{ success: boolean; session?: SessionDetails; error?: string }> {\n try {\n const session = await loadSession(sessionId, sessionsDir);\n\n const databases: DatabaseDetails[] = [];\n for (const [provider, dbStatus] of Object.entries(session.databases)) {\n if (!dbStatus) continue;\n const dbDetail: DatabaseDetails = {\n provider,\n status: dbStatus.status,\n totalHits: dbStatus.totalHits ?? 0,\n retrievedCount: dbStatus.retrievedCount ?? 0,\n };\n if (dbStatus.error?.message) {\n dbDetail.error = dbStatus.error.message;\n }\n databases.push(dbDetail);\n }\n\n const sessionDetails: SessionDetails = {\n id: session.id,\n name: session.name,\n status: session.summary.status,\n createdAt: session.createdAt,\n updatedAt: session.updatedAt,\n queryFile: session.query.file,\n totalHits: session.summary.totalHits,\n totalRetrieved: session.summary.totalRetrieved,\n databases,\n };\n if (session.description) {\n sessionDetails.description = session.description;\n }\n\n // Load notes (optional - don't fail if notes can't be loaded)\n try {\n const sessionDir = join(sessionsDir, sessionId);\n const notes = await loadNotes(sessionDir);\n if (notes.length > 0) {\n sessionDetails.notes = notes;\n }\n } catch {\n // Notes are optional - ignore errors\n }\n\n return {\n success: true,\n session: sessionDetails,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n return {\n success: false,\n error: message,\n };\n }\n}\n\nexport async function computeDeduplicationStats(\n sessionId: string,\n sessionsDir: string,\n session: { databases: Record<string, { files?: { results?: string } } | undefined> }\n): Promise<{ uniqueArticles: number; duplicatesRemoved: number }> {\n const articles: import('../../providers/base/types.js').Article[] = [];\n const sessionDir = join(sessionsDir, sessionId);\n\n for (const [providerName] of Object.entries(session.databases)) {\n const providerArticles = await loadResults(sessionDir, providerName as ProviderName);\n articles.push(...providerArticles);\n }\n\n if (articles.length === 0) {\n return { uniqueArticles: 0, duplicatesRemoved: 0 };\n }\n\n const result = deduplicateArticles(articles);\n return {\n uniqueArticles: result.articles.length,\n duplicatesRemoved: result.duplicatesRemoved,\n };\n}\n\nexport function formatSessionList(\n result: SessionListResult,\n options: FormatOptions\n): string {\n if (options.json) {\n return JSON.stringify(result.sessions, null, 2);\n }\n\n if (result.sessions.length === 0) {\n if (result.totalCount === 0) {\n return 'No sessions found.';\n }\n // Sessions exist but are filtered out (all completed)\n const completedCount = result.totalCount - result.filteredCount;\n return `No active sessions. ${completedCount} completed session${completedCount === 1 ? '' : 's'} hidden (use --all to show).`;\n }\n\n const header = `${'ID'.padEnd(35)} ${'NAME'.padEnd(20)} ${'STATUS'.padEnd(15)} ${'PROGRESS'.padEnd(12)} CREATED`;\n const separator = '-'.repeat(100);\n\n const rows = result.sessions.map((s) => {\n const date = new Date(s.createdAt).toLocaleDateString();\n return `${s.id.padEnd(35)} ${s.name.padEnd(20)} ${s.status.padEnd(15)} ${s.progress.padEnd(12)} ${date}`;\n });\n\n const lines = [header, separator, ...rows];\n\n // Show hint about hidden completed sessions if not showing all\n if (!result.showingAll && result.totalCount > result.filteredCount) {\n const hiddenCount = result.totalCount - result.filteredCount;\n lines.push('');\n lines.push(`(${hiddenCount} completed session${hiddenCount === 1 ? '' : 's'} hidden, use --all to show)`);\n }\n\n return lines.join('\\n');\n}\n\nexport function formatSessionDetails(\n details: SessionDetails,\n options: FormatOptions\n): string {\n if (options.json) {\n return JSON.stringify(details, null, 2);\n }\n\n const lines: string[] = [];\n\n lines.push(`Session: ${details.name}`);\n lines.push(`ID: ${details.id}`);\n if (details.description) {\n lines.push(`Description: ${details.description}`);\n }\n lines.push(`Status: ${details.status}`);\n lines.push(`Query File: ${details.queryFile}`);\n lines.push(`Created: ${new Date(details.createdAt).toLocaleString()}`);\n lines.push(`Updated: ${new Date(details.updatedAt).toLocaleString()}`);\n lines.push('');\n if (details.duplicatesRemoved !== undefined && details.duplicatesRemoved > 0) {\n lines.push(`Total: ${details.totalRetrieved} raw / ${details.uniqueArticles} unique (${details.duplicatesRemoved} duplicates)`);\n } else {\n lines.push(`Total: ${details.totalRetrieved}/${details.totalHits} results`);\n }\n lines.push('');\n lines.push('Databases:');\n\n for (const db of details.databases) {\n const statusIcon = getStatusIcon(db.status);\n let line = ` ${statusIcon} ${db.provider.padEnd(10)} ${db.status.padEnd(12)} ${db.retrievedCount}/${db.totalHits}`;\n if (db.error) {\n line += ` (${db.error})`;\n }\n lines.push(line);\n }\n\n // Display notes if present\n if (details.notes && details.notes.length > 0) {\n lines.push('');\n lines.push('Notes:');\n lines.push(formatNotesList(details.notes));\n }\n\n return lines.join('\\n');\n}\n\nfunction getStatusIcon(status: string): string {\n switch (status) {\n case 'completed':\n return '\\u2713'; // ✓\n case 'failed':\n return '\\u2717'; // ✗\n case 'in_progress':\n return '\\u280B'; // ⠋\n case 'pending':\n return '\\u25FC'; // ◼\n default:\n return ' ';\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/parser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,oBAAoB,EAAE,cAAc,EAAE,cAAc,EAAgB,MAAM,SAAS,CAAC;AAgDlG;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,oBAAoB,CA0BvE;AAgBD;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,CAqEnE"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/parser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,oBAAoB,EAAE,cAAc,EAAE,cAAc,EAAgB,MAAM,SAAS,CAAC;AAgDlG;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,oBAAoB,CAiCvE;AAgBD;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,CAqEnE"}
@@ -1,24 +1,24 @@
1
1
  import { z } from "zod";
2
2
  const ScopusAuthorSchema = z.object({
3
- authname: z.string().optional(),
4
- authid: z.string().optional(),
5
- afid: z.array(z.object({ $: z.string().optional() })).optional()
3
+ authname: z.string().nullish(),
4
+ authid: z.string().nullish(),
5
+ afid: z.array(z.object({ $: z.string().nullish() })).nullish()
6
6
  });
7
7
  const ScopusRawEntrySchema = z.object({
8
- "dc:identifier": z.string().optional(),
9
- "dc:title": z.string().optional(),
10
- "dc:creator": z.string().optional(),
11
- "dc:description": z.string().optional(),
12
- "prism:doi": z.string().optional(),
13
- "prism:coverDate": z.string().optional(),
14
- "prism:publicationName": z.string().optional(),
15
- "prism:volume": z.string().optional(),
16
- "prism:issueIdentifier": z.string().optional(),
17
- "prism:pageRange": z.string().optional(),
18
- "citedby-count": z.string().optional(),
19
- eid: z.string().optional(),
20
- subtypeDescription: z.string().optional(),
21
- author: z.array(ScopusAuthorSchema).optional()
8
+ "dc:identifier": z.string().nullish(),
9
+ "dc:title": z.string().nullish(),
10
+ "dc:creator": z.string().nullish(),
11
+ "dc:description": z.string().nullish(),
12
+ "prism:doi": z.string().nullish(),
13
+ "prism:coverDate": z.string().nullish(),
14
+ "prism:publicationName": z.string().nullish(),
15
+ "prism:volume": z.string().nullish(),
16
+ "prism:issueIdentifier": z.string().nullish(),
17
+ "prism:pageRange": z.string().nullish(),
18
+ "citedby-count": z.string().nullish(),
19
+ eid: z.string().nullish(),
20
+ subtypeDescription: z.string().nullish(),
21
+ author: z.array(ScopusAuthorSchema).nullish()
22
22
  });
23
23
  const ScopusSearchResultsSchema = z.object({
24
24
  "opensearch:totalResults": z.string().optional(),
@@ -32,11 +32,14 @@ const ScopusApiResponseSchema = z.object({
32
32
  function parseSearchResponse(json) {
33
33
  const parseResult = ScopusApiResponseSchema.safeParse(json);
34
34
  if (!parseResult.success) {
35
+ const issues = parseResult.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
36
+ const warning = `Scopus API response parse failed: ${issues}`;
35
37
  return {
36
38
  totalResults: 0,
37
39
  startIndex: 0,
38
40
  itemsPerPage: 25,
39
- entries: []
41
+ entries: [],
42
+ parseWarning: warning
40
43
  };
41
44
  }
42
45
  const searchResults = parseResult.data["search-results"];
@@ -1 +1 @@
1
- {"version":3,"file":"parser.js","sources":["../../../src/providers/scopus/parser.ts"],"sourcesContent":["/**\n * Scopus Response Parser\n *\n * Parses Scopus API JSON responses into typed structures.\n */\n\nimport { z } from 'zod';\nimport type { ScopusSearchResponse, ScopusDocument, ScopusRawEntry, ScopusAuthor } from './types';\n\n/**\n * Zod schema for Scopus author in API response.\n */\nconst ScopusAuthorSchema = z.object({\n authname: z.string().optional(),\n authid: z.string().optional(),\n afid: z.array(z.object({ $: z.string().optional() })).optional(),\n});\n\n/**\n * Zod schema for Scopus entry in API response.\n */\nconst ScopusRawEntrySchema = z.object({\n 'dc:identifier': z.string().optional(),\n 'dc:title': z.string().optional(),\n 'dc:creator': z.string().optional(),\n 'dc:description': z.string().optional(),\n 'prism:doi': z.string().optional(),\n 'prism:coverDate': z.string().optional(),\n 'prism:publicationName': z.string().optional(),\n 'prism:volume': z.string().optional(),\n 'prism:issueIdentifier': z.string().optional(),\n 'prism:pageRange': z.string().optional(),\n 'citedby-count': z.string().optional(),\n eid: z.string().optional(),\n subtypeDescription: z.string().optional(),\n author: z.array(ScopusAuthorSchema).optional(),\n});\n\n/**\n * Zod schema for Scopus search results wrapper.\n */\nconst ScopusSearchResultsSchema = z.object({\n 'opensearch:totalResults': z.string().optional(),\n 'opensearch:startIndex': z.string().optional(),\n 'opensearch:itemsPerPage': z.string().optional(),\n entry: z.array(ScopusRawEntrySchema).optional(),\n});\n\n/**\n * Zod schema for full Scopus API response.\n */\nconst ScopusApiResponseSchema = z.object({\n 'search-results': ScopusSearchResultsSchema,\n});\n\n/**\n * Parse the raw Scopus API search response.\n * Validates the response structure using Zod before parsing.\n */\nexport function parseSearchResponse(json: unknown): ScopusSearchResponse {\n const parseResult = ScopusApiResponseSchema.safeParse(json);\n\n if (!parseResult.success) {\n // Fallback to empty response on invalid structure\n return {\n totalResults: 0,\n startIndex: 0,\n itemsPerPage: 25,\n entries: [],\n };\n }\n\n const searchResults = parseResult.data['search-results'];\n\n const totalResults = parseInt(searchResults['opensearch:totalResults'] ?? '0', 10) || 0;\n const startIndex = parseInt(searchResults['opensearch:startIndex'] ?? '0', 10) || 0;\n const itemsPerPage = parseInt(searchResults['opensearch:itemsPerPage'] ?? '25', 10) || 25;\n const entries = (searchResults.entry as ScopusRawEntry[]) ?? [];\n\n return {\n totalResults,\n startIndex,\n itemsPerPage,\n entries,\n };\n}\n\n/**\n * Parse author name from \"Last, First\" format.\n */\nfunction parseAuthorName(authname: string): { family: string; given?: string | undefined } {\n const parts = authname.split(', ');\n if (parts.length >= 2) {\n return {\n family: parts[0]!,\n given: parts.slice(1).join(', '),\n };\n }\n return { family: authname };\n}\n\n/**\n * Parse a single document entry into ScopusDocument.\n */\nexport function parseDocument(entry: ScopusRawEntry): ScopusDocument {\n // Parse authors\n let authors: ScopusAuthor[];\n\n if (entry.author && entry.author.length > 0) {\n authors = entry.author.map(a => {\n const parsed = parseAuthorName(a.authname || '');\n const author: ScopusAuthor = {\n family: parsed.family,\n };\n if (parsed.given !== undefined) {\n author.given = parsed.given;\n }\n if (a.authid !== undefined) {\n author.authid = a.authid;\n }\n return author;\n });\n } else if (entry['dc:creator']) {\n // Fallback to dc:creator\n authors = [{ family: entry['dc:creator'] }];\n } else {\n authors = [];\n }\n\n // Build document with required fields\n const doc: ScopusDocument = {\n scopusId: entry['dc:identifier'] || '',\n title: entry['dc:title'] || '',\n authors,\n source: 'scopus',\n retrievedAt: new Date().toISOString(),\n };\n\n // Add optional fields only if defined\n if (entry['dc:description'] !== undefined) {\n doc.abstract = entry['dc:description'];\n }\n if (entry['prism:doi'] !== undefined) {\n doc.doi = entry['prism:doi'];\n }\n if (entry['prism:coverDate'] !== undefined) {\n doc.publicationDate = entry['prism:coverDate'];\n }\n if (entry['prism:publicationName'] !== undefined) {\n doc.journal = entry['prism:publicationName'];\n }\n if (entry['prism:volume'] !== undefined) {\n doc.volume = entry['prism:volume'];\n }\n if (entry['prism:issueIdentifier'] !== undefined) {\n doc.issue = entry['prism:issueIdentifier'];\n }\n if (entry['prism:pageRange'] !== undefined) {\n doc.pages = entry['prism:pageRange'];\n }\n\n // Scopus-specific fields\n if (entry['citedby-count'] !== undefined) {\n doc.citedByCount = parseInt(entry['citedby-count'], 10);\n }\n if (entry.eid !== undefined) {\n doc.eid = entry.eid;\n }\n if (entry.subtypeDescription !== undefined) {\n doc.sourceType = entry.subtypeDescription;\n }\n\n return doc;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"parser.js","sources":["../../../src/providers/scopus/parser.ts"],"sourcesContent":["/**\n * Scopus Response Parser\n *\n * Parses Scopus API JSON responses into typed structures.\n */\n\nimport { z } from 'zod';\nimport type { ScopusSearchResponse, ScopusDocument, ScopusRawEntry, ScopusAuthor } from './types';\n\n/**\n * Zod schema for Scopus author in API response.\n */\nconst ScopusAuthorSchema = z.object({\n authname: z.string().nullish(),\n authid: z.string().nullish(),\n afid: z.array(z.object({ $: z.string().nullish() })).nullish(),\n});\n\n/**\n * Zod schema for Scopus entry in API response.\n */\nconst ScopusRawEntrySchema = z.object({\n 'dc:identifier': z.string().nullish(),\n 'dc:title': z.string().nullish(),\n 'dc:creator': z.string().nullish(),\n 'dc:description': z.string().nullish(),\n 'prism:doi': z.string().nullish(),\n 'prism:coverDate': z.string().nullish(),\n 'prism:publicationName': z.string().nullish(),\n 'prism:volume': z.string().nullish(),\n 'prism:issueIdentifier': z.string().nullish(),\n 'prism:pageRange': z.string().nullish(),\n 'citedby-count': z.string().nullish(),\n eid: z.string().nullish(),\n subtypeDescription: z.string().nullish(),\n author: z.array(ScopusAuthorSchema).nullish(),\n});\n\n/**\n * Zod schema for Scopus search results wrapper.\n */\nconst ScopusSearchResultsSchema = z.object({\n 'opensearch:totalResults': z.string().optional(),\n 'opensearch:startIndex': z.string().optional(),\n 'opensearch:itemsPerPage': z.string().optional(),\n entry: z.array(ScopusRawEntrySchema).optional(),\n});\n\n/**\n * Zod schema for full Scopus API response.\n */\nconst ScopusApiResponseSchema = z.object({\n 'search-results': ScopusSearchResultsSchema,\n});\n\n/**\n * Parse the raw Scopus API search response.\n * Validates the response structure using Zod before parsing.\n */\nexport function parseSearchResponse(json: unknown): ScopusSearchResponse {\n const parseResult = ScopusApiResponseSchema.safeParse(json);\n\n if (!parseResult.success) {\n // Build warning message from Zod errors\n const issues = parseResult.error.issues\n .map(issue => `${issue.path.join('.')}: ${issue.message}`)\n .join('; ');\n const warning = `Scopus API response parse failed: ${issues}`;\n\n // Fallback to empty response on invalid structure\n return {\n totalResults: 0,\n startIndex: 0,\n itemsPerPage: 25,\n entries: [],\n parseWarning: warning,\n };\n }\n\n const searchResults = parseResult.data['search-results'];\n\n const totalResults = parseInt(searchResults['opensearch:totalResults'] ?? '0', 10) || 0;\n const startIndex = parseInt(searchResults['opensearch:startIndex'] ?? '0', 10) || 0;\n const itemsPerPage = parseInt(searchResults['opensearch:itemsPerPage'] ?? '25', 10) || 25;\n const entries = (searchResults.entry as ScopusRawEntry[]) ?? [];\n\n return {\n totalResults,\n startIndex,\n itemsPerPage,\n entries,\n };\n}\n\n/**\n * Parse author name from \"Last, First\" format.\n */\nfunction parseAuthorName(authname: string): { family: string; given?: string | undefined } {\n const parts = authname.split(', ');\n if (parts.length >= 2) {\n return {\n family: parts[0]!,\n given: parts.slice(1).join(', '),\n };\n }\n return { family: authname };\n}\n\n/**\n * Parse a single document entry into ScopusDocument.\n */\nexport function parseDocument(entry: ScopusRawEntry): ScopusDocument {\n // Parse authors\n let authors: ScopusAuthor[];\n\n if (entry.author && entry.author.length > 0) {\n authors = entry.author.map(a => {\n const parsed = parseAuthorName(a.authname || '');\n const author: ScopusAuthor = {\n family: parsed.family,\n };\n if (parsed.given !== undefined) {\n author.given = parsed.given;\n }\n if (a.authid !== undefined) {\n author.authid = a.authid;\n }\n return author;\n });\n } else if (entry['dc:creator']) {\n // Fallback to dc:creator\n authors = [{ family: entry['dc:creator'] }];\n } else {\n authors = [];\n }\n\n // Build document with required fields\n const doc: ScopusDocument = {\n scopusId: entry['dc:identifier'] || '',\n title: entry['dc:title'] || '',\n authors,\n source: 'scopus',\n retrievedAt: new Date().toISOString(),\n };\n\n // Add optional fields only if defined\n if (entry['dc:description'] !== undefined) {\n doc.abstract = entry['dc:description'];\n }\n if (entry['prism:doi'] !== undefined) {\n doc.doi = entry['prism:doi'];\n }\n if (entry['prism:coverDate'] !== undefined) {\n doc.publicationDate = entry['prism:coverDate'];\n }\n if (entry['prism:publicationName'] !== undefined) {\n doc.journal = entry['prism:publicationName'];\n }\n if (entry['prism:volume'] !== undefined) {\n doc.volume = entry['prism:volume'];\n }\n if (entry['prism:issueIdentifier'] !== undefined) {\n doc.issue = entry['prism:issueIdentifier'];\n }\n if (entry['prism:pageRange'] !== undefined) {\n doc.pages = entry['prism:pageRange'];\n }\n\n // Scopus-specific fields\n if (entry['citedby-count'] !== undefined) {\n doc.citedByCount = parseInt(entry['citedby-count'], 10);\n }\n if (entry.eid !== undefined) {\n doc.eid = entry.eid;\n }\n if (entry.subtypeDescription !== undefined) {\n doc.sourceType = entry.subtypeDescription;\n }\n\n return doc;\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAA2B,MAAM,kBAAkB,CAAC;AACzE,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,aAAa,EACb,QAAQ,EACR,WAAW,EACX,kBAAkB,EAClB,oBAAoB,EACrB,MAAM,eAAe,CAAC;AAIvB,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,SAAS,CAAC;AAKjE;;GAEG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAElC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAE5C,8CAA8C;IAC9C,OAAO,CAAC,YAAY,CAA4B;gBAEpC,MAAM,EAAE,YAAY;IAuBhC;;OAEG;IACH,cAAc,CAAC,GAAG,EAAE,QAAQ,GAAG,eAAe;IAI9C;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAQpD;;OAEG;IACI,MAAM,CACX,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE,aAAkB,GAC1B,aAAa,CAAC,OAAO,CAAC;IA0EzB;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAIrD;;OAEG;IACH,cAAc,IAAI,WAAW,GAAG,IAAI;IAIpC;;OAEG;IACI,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,aAAa,CAAC,OAAO,CAAC;IAqE/D;;OAEG;IACG,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAsBrE"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAA2B,MAAM,kBAAkB,CAAC;AACzE,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,aAAa,EACb,QAAQ,EACR,WAAW,EACX,kBAAkB,EAClB,oBAAoB,EACrB,MAAM,eAAe,CAAC;AAIvB,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,SAAS,CAAC;AAKjE;;GAEG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAElC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAE5C,8CAA8C;IAC9C,OAAO,CAAC,YAAY,CAA4B;gBAEpC,MAAM,EAAE,YAAY;IAuBhC;;OAEG;IACH,cAAc,CAAC,GAAG,EAAE,QAAQ,GAAG,eAAe;IAI9C;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAWpD;;OAEG;IACI,MAAM,CACX,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE,aAAkB,GAC1B,aAAa,CAAC,OAAO,CAAC;IA6EzB;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAIrD;;OAEG;IACH,cAAc,IAAI,WAAW,GAAG,IAAI;IAIpC;;OAEG;IACI,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,aAAa,CAAC,OAAO,CAAC;IAqE/D;;OAEG;IACG,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAsBrE"}
@@ -45,6 +45,9 @@ class ScopusProvider extends BaseProvider {
45
45
  const response = await this.withRetry(async () => {
46
46
  return await this.client.search(query.native, { start: 0, count: 1 });
47
47
  });
48
+ if (response.parseWarning) {
49
+ console.error(`Warning: ${response.parseWarning}`);
50
+ }
48
51
  return response.totalResults;
49
52
  }
50
53
  /**
@@ -67,6 +70,9 @@ class ScopusProvider extends BaseProvider {
67
70
  });
68
71
  if (offset === 0) {
69
72
  totalResults = response.totalResults;
73
+ if (response.parseWarning) {
74
+ console.error(`Warning: ${response.parseWarning}`);
75
+ }
70
76
  }
71
77
  this.currentState = {
72
78
  ...this.createBaseState(query, totalResults, retrievedCount),
@@ -1 +1 @@
1
- {"version":3,"file":"provider.js","sources":["../../../src/providers/scopus/provider.ts"],"sourcesContent":["/**\n * Scopus Provider\n *\n * Implements the Provider interface for Scopus database searches.\n */\n\nimport { BaseProvider, type BaseProviderConfig } from '../base/provider';\nimport type {\n Article,\n TranslatedQuery,\n SearchOptions,\n QueryAST,\n SearchState,\n SearchResumeResult,\n ConnectionTestResult,\n} from '../base/types';\nimport { ScopusClient } from './client';\nimport { parseDocument } from './parser';\nimport { translateQuery } from './translator';\nimport type { ScopusConfig, ScopusProviderState } from './types';\n\n/** Default page size for Scopus (max 25 for COMPLETE view) */\nconst DEFAULT_PAGE_SIZE = 25;\n\n/**\n * Scopus database provider.\n */\nexport class ScopusProvider extends BaseProvider {\n readonly name = 'scopus' as const;\n\n private readonly client: ScopusClient;\n private readonly scopusConfig: ScopusConfig;\n\n /** Current search state for resume support */\n private currentState: SearchState | null = null;\n\n constructor(config: ScopusConfig) {\n const baseConfig: BaseProviderConfig = {};\n if (config.rateLimit !== undefined) {\n baseConfig.rateLimit = config.rateLimit;\n }\n if (config.timeout !== undefined) {\n baseConfig.timeout = config.timeout;\n }\n if (config.retries !== undefined) {\n baseConfig.retries = config.retries;\n }\n if (config.initialBackoff !== undefined) {\n baseConfig.initialBackoff = config.initialBackoff;\n }\n if (config.maxBackoff !== undefined) {\n baseConfig.maxBackoff = config.maxBackoff;\n }\n super(baseConfig);\n\n this.scopusConfig = config;\n this.client = new ScopusClient(config);\n }\n\n /**\n * Translate QueryAST to Scopus search syntax.\n */\n translateQuery(ast: QueryAST): TranslatedQuery {\n return translateQuery(ast);\n }\n\n /**\n * Get total hit count for a query without downloading results.\n * Uses a minimal search with count=1 to get the total from response metadata.\n */\n async count(query: TranslatedQuery): Promise<number> {\n await this.rateLimiter.acquire();\n const response = await this.withRetry(async () => {\n return await this.client.search(query.native, { start: 0, count: 1 });\n });\n return response.totalResults;\n }\n\n /**\n * Execute search and return results as async iterable.\n */\n async *search(\n query: TranslatedQuery,\n options: SearchOptions = {}\n ): AsyncIterable<Article> {\n const maxResults = options.maxResults ?? this.scopusConfig.maxResults ?? 10000;\n const pageSize = Math.min(options.pageSize ?? DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE);\n\n let offset = 0;\n let totalResults = 0;\n let retrievedCount = 0;\n\n // Initialize state\n this.currentState = this.createBaseState(query, 0, 0);\n\n while (retrievedCount < maxResults) {\n // Wait for rate limiter\n await this.rateLimiter.acquire();\n\n // Fetch page with retry\n const response = await this.withRetry(async () => {\n return await this.client.search(query.native, {\n start: offset,\n count: Math.min(pageSize, maxResults - retrievedCount),\n });\n });\n\n // Update total on first page\n if (offset === 0) {\n totalResults = response.totalResults;\n }\n\n // Update state\n this.currentState = {\n ...this.createBaseState(query, totalResults, retrievedCount),\n providerState: {\n offset,\n totalResults,\n query: query.native,\n } as ScopusProviderState,\n };\n\n // Yield articles\n for (const entry of response.entries) {\n if (retrievedCount >= maxResults) {\n break;\n }\n\n const doc = parseDocument(entry);\n retrievedCount++;\n\n // Update state before yield so it's available when consumer breaks\n if (this.currentState) {\n this.currentState.retrievedCount = retrievedCount;\n this.currentState.lastUpdated = new Date();\n }\n\n yield doc;\n }\n\n // Move to next page\n offset += response.entries.length;\n\n // Check if we've retrieved all results\n if (offset >= totalResults || response.entries.length === 0) {\n break;\n }\n\n // Check abort signal\n if (options.signal?.aborted) {\n break;\n }\n }\n\n // Clear state when search completes\n this.currentState = null;\n }\n\n /**\n * Verify API access and credentials.\n */\n async testConnection(): Promise<ConnectionTestResult> {\n return this.client.testConnection();\n }\n\n /**\n * Get current search state for session persistence.\n */\n getSearchState(): SearchState | null {\n return this.currentState;\n }\n\n /**\n * Resume search from saved state.\n */\n async *resumeSearch(state: SearchState): AsyncIterable<Article> {\n const providerState = state.providerState as ScopusProviderState;\n if (!providerState) {\n throw new Error('Invalid state: missing provider state');\n }\n\n const maxResults = state.totalResults;\n const pageSize = DEFAULT_PAGE_SIZE;\n\n let offset = state.retrievedCount;\n let retrievedCount = state.retrievedCount;\n\n // Restore state\n this.currentState = { ...state };\n\n while (retrievedCount < maxResults) {\n // Wait for rate limiter\n await this.rateLimiter.acquire();\n\n // Fetch page with retry\n const response = await this.withRetry(async () => {\n return await this.client.search(providerState.query, {\n start: offset,\n count: Math.min(pageSize, maxResults - retrievedCount),\n });\n });\n\n // Update state\n this.currentState = {\n ...this.currentState!,\n retrievedCount,\n lastUpdated: new Date(),\n providerState: {\n ...providerState,\n offset,\n },\n };\n\n // Yield articles\n for (const entry of response.entries) {\n if (retrievedCount >= maxResults) {\n break;\n }\n\n const doc = parseDocument(entry);\n retrievedCount++;\n\n // Update state before yield so it's available when consumer breaks\n if (this.currentState) {\n this.currentState.retrievedCount = retrievedCount;\n this.currentState.lastUpdated = new Date();\n }\n\n yield doc;\n }\n\n // Move to next page\n offset += response.entries.length;\n\n // Check if we've retrieved all results\n if (offset >= maxResults || response.entries.length === 0) {\n break;\n }\n }\n\n // Clear state when search completes\n this.currentState = null;\n }\n\n /**\n * Validate if a saved state is still valid for resuming.\n */\n async validateState(state: SearchState): Promise<SearchResumeResult> {\n // Check if the API key is still valid\n const connectionValid = await this.testConnection();\n if (!connectionValid) {\n return {\n valid: false,\n reason: 'API key is invalid or connection failed',\n };\n }\n\n // For Scopus, offset-based pagination is always valid if the API key works\n // We don't need to check server-side state like PubMed's WebEnv\n const providerState = state.providerState as ScopusProviderState;\n if (!providerState) {\n return {\n valid: false,\n reason: 'Missing provider state',\n };\n }\n\n return { valid: true };\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"provider.js","sources":["../../../src/providers/scopus/provider.ts"],"sourcesContent":["/**\n * Scopus Provider\n *\n * Implements the Provider interface for Scopus database searches.\n */\n\nimport { BaseProvider, type BaseProviderConfig } from '../base/provider';\nimport type {\n Article,\n TranslatedQuery,\n SearchOptions,\n QueryAST,\n SearchState,\n SearchResumeResult,\n ConnectionTestResult,\n} from '../base/types';\nimport { ScopusClient } from './client';\nimport { parseDocument } from './parser';\nimport { translateQuery } from './translator';\nimport type { ScopusConfig, ScopusProviderState } from './types';\n\n/** Default page size for Scopus (max 25 for COMPLETE view) */\nconst DEFAULT_PAGE_SIZE = 25;\n\n/**\n * Scopus database provider.\n */\nexport class ScopusProvider extends BaseProvider {\n readonly name = 'scopus' as const;\n\n private readonly client: ScopusClient;\n private readonly scopusConfig: ScopusConfig;\n\n /** Current search state for resume support */\n private currentState: SearchState | null = null;\n\n constructor(config: ScopusConfig) {\n const baseConfig: BaseProviderConfig = {};\n if (config.rateLimit !== undefined) {\n baseConfig.rateLimit = config.rateLimit;\n }\n if (config.timeout !== undefined) {\n baseConfig.timeout = config.timeout;\n }\n if (config.retries !== undefined) {\n baseConfig.retries = config.retries;\n }\n if (config.initialBackoff !== undefined) {\n baseConfig.initialBackoff = config.initialBackoff;\n }\n if (config.maxBackoff !== undefined) {\n baseConfig.maxBackoff = config.maxBackoff;\n }\n super(baseConfig);\n\n this.scopusConfig = config;\n this.client = new ScopusClient(config);\n }\n\n /**\n * Translate QueryAST to Scopus search syntax.\n */\n translateQuery(ast: QueryAST): TranslatedQuery {\n return translateQuery(ast);\n }\n\n /**\n * Get total hit count for a query without downloading results.\n * Uses a minimal search with count=1 to get the total from response metadata.\n */\n async count(query: TranslatedQuery): Promise<number> {\n await this.rateLimiter.acquire();\n const response = await this.withRetry(async () => {\n return await this.client.search(query.native, { start: 0, count: 1 });\n });\n if (response.parseWarning) {\n console.error(`Warning: ${response.parseWarning}`);\n }\n return response.totalResults;\n }\n\n /**\n * Execute search and return results as async iterable.\n */\n async *search(\n query: TranslatedQuery,\n options: SearchOptions = {}\n ): AsyncIterable<Article> {\n const maxResults = options.maxResults ?? this.scopusConfig.maxResults ?? 10000;\n const pageSize = Math.min(options.pageSize ?? DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE);\n\n let offset = 0;\n let totalResults = 0;\n let retrievedCount = 0;\n\n // Initialize state\n this.currentState = this.createBaseState(query, 0, 0);\n\n while (retrievedCount < maxResults) {\n // Wait for rate limiter\n await this.rateLimiter.acquire();\n\n // Fetch page with retry\n const response = await this.withRetry(async () => {\n return await this.client.search(query.native, {\n start: offset,\n count: Math.min(pageSize, maxResults - retrievedCount),\n });\n });\n\n // Update total on first page and check for parse warnings\n if (offset === 0) {\n totalResults = response.totalResults;\n if (response.parseWarning) {\n console.error(`Warning: ${response.parseWarning}`);\n }\n }\n\n // Update state\n this.currentState = {\n ...this.createBaseState(query, totalResults, retrievedCount),\n providerState: {\n offset,\n totalResults,\n query: query.native,\n } as ScopusProviderState,\n };\n\n // Yield articles\n for (const entry of response.entries) {\n if (retrievedCount >= maxResults) {\n break;\n }\n\n const doc = parseDocument(entry);\n retrievedCount++;\n\n // Update state before yield so it's available when consumer breaks\n if (this.currentState) {\n this.currentState.retrievedCount = retrievedCount;\n this.currentState.lastUpdated = new Date();\n }\n\n yield doc;\n }\n\n // Move to next page\n offset += response.entries.length;\n\n // Check if we've retrieved all results\n if (offset >= totalResults || response.entries.length === 0) {\n break;\n }\n\n // Check abort signal\n if (options.signal?.aborted) {\n break;\n }\n }\n\n // Clear state when search completes\n this.currentState = null;\n }\n\n /**\n * Verify API access and credentials.\n */\n async testConnection(): Promise<ConnectionTestResult> {\n return this.client.testConnection();\n }\n\n /**\n * Get current search state for session persistence.\n */\n getSearchState(): SearchState | null {\n return this.currentState;\n }\n\n /**\n * Resume search from saved state.\n */\n async *resumeSearch(state: SearchState): AsyncIterable<Article> {\n const providerState = state.providerState as ScopusProviderState;\n if (!providerState) {\n throw new Error('Invalid state: missing provider state');\n }\n\n const maxResults = state.totalResults;\n const pageSize = DEFAULT_PAGE_SIZE;\n\n let offset = state.retrievedCount;\n let retrievedCount = state.retrievedCount;\n\n // Restore state\n this.currentState = { ...state };\n\n while (retrievedCount < maxResults) {\n // Wait for rate limiter\n await this.rateLimiter.acquire();\n\n // Fetch page with retry\n const response = await this.withRetry(async () => {\n return await this.client.search(providerState.query, {\n start: offset,\n count: Math.min(pageSize, maxResults - retrievedCount),\n });\n });\n\n // Update state\n this.currentState = {\n ...this.currentState!,\n retrievedCount,\n lastUpdated: new Date(),\n providerState: {\n ...providerState,\n offset,\n },\n };\n\n // Yield articles\n for (const entry of response.entries) {\n if (retrievedCount >= maxResults) {\n break;\n }\n\n const doc = parseDocument(entry);\n retrievedCount++;\n\n // Update state before yield so it's available when consumer breaks\n if (this.currentState) {\n this.currentState.retrievedCount = retrievedCount;\n this.currentState.lastUpdated = new Date();\n }\n\n yield doc;\n }\n\n // Move to next page\n offset += response.entries.length;\n\n // Check if we've retrieved all results\n if (offset >= maxResults || response.entries.length === 0) {\n break;\n }\n }\n\n // Clear state when search completes\n this.currentState = null;\n }\n\n /**\n * Validate if a saved state is still valid for resuming.\n */\n async validateState(state: SearchState): Promise<SearchResumeResult> {\n // Check if the API key is still valid\n const connectionValid = await this.testConnection();\n if (!connectionValid) {\n return {\n valid: false,\n reason: 'API key is invalid or connection failed',\n };\n }\n\n // For Scopus, offset-based pagination is always valid if the API key works\n // We don't need to check server-side state like PubMed's WebEnv\n const providerState = state.providerState as ScopusProviderState;\n if (!providerState) {\n return {\n valid: false,\n reason: 'Missing provider state',\n };\n }\n\n return { valid: true };\n }\n}\n"],"names":[],"mappings":"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"}
@@ -60,6 +60,8 @@ export interface ScopusSearchResponse {
60
60
  itemsPerPage: number;
61
61
  /** Array of result entries */
62
62
  entries: ScopusRawEntry[];
63
+ /** Warning message if parse failed or had issues */
64
+ parseWarning?: string | undefined;
63
65
  }
64
66
  /**
65
67
  * Configuration for Scopus provider.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D;;GAEG;AACH,MAAM,WAAW,YAAa,SAAQ,MAAM;IAC1C,uBAAuB;IACvB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,WAAW,cAAe,SAAQ,OAAO;IAC7C,qCAAqC;IACrC,OAAO,EAAE,YAAY,EAAE,CAAC;IAExB,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IAEjB,qBAAqB;IACrB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAElC,yCAAyC;IACzC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEzB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,KAAK,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,KAAK,CAAC;YAAE,CAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAChC,CAAC,CAAC;CACJ;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,8BAA8B;IAC9B,YAAY,EAAE,MAAM,CAAC;IAErB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IAEnB,+BAA+B;IAC/B,YAAY,EAAE,MAAM,CAAC;IAErB,8BAA8B;IAC9B,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,YAAa,SAAQ,kBAAkB;IACtD,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC;IAEf,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IAEf,0CAA0C;IAC1C,YAAY,EAAE,MAAM,CAAC;IAErB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/providers/scopus/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D;;GAEG;AACH,MAAM,WAAW,YAAa,SAAQ,MAAM;IAC1C,uBAAuB;IACvB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,WAAW,cAAe,SAAQ,OAAO;IAC7C,qCAAqC;IACrC,OAAO,EAAE,YAAY,EAAE,CAAC;IAExB,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IAEjB,qBAAqB;IACrB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAElC,yCAAyC;IACzC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEzB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,KAAK,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,KAAK,CAAC;YAAE,CAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAChC,CAAC,CAAC;CACJ;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,8BAA8B;IAC9B,YAAY,EAAE,MAAM,CAAC;IAErB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IAEnB,+BAA+B;IAC/B,YAAY,EAAE,MAAM,CAAC;IAErB,8BAA8B;IAC9B,OAAO,EAAE,cAAc,EAAE,CAAC;IAE1B,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,YAAa,SAAQ,kBAAkB;IACtD,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC;IAEf,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IAEf,0CAA0C;IAC1C,YAAY,EAAE,MAAM,CAAC;IAErB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ncukondo/search-hub",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "A CLI tool for systematic literature searching across multiple academic databases",
5
5
  "type": "module",
6
6
  "engines": {