@ncukondo/search-hub 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/merge.d.ts +82 -0
- package/dist/cli/commands/merge.d.ts.map +1 -0
- package/dist/cli/commands/merge.js +221 -0
- package/dist/cli/commands/merge.js.map +1 -0
- package/dist/cli/commands/resume.d.ts.map +1 -1
- package/dist/cli/commands/resume.js +7 -0
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +146 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/suggestions/rules.d.ts.map +1 -1
- package/dist/cli/suggestions/rules.js +20 -2
- package/dist/cli/suggestions/rules.js.map +1 -1
- package/dist/cli/suggestions/types.d.ts +6 -0
- package/dist/cli/suggestions/types.d.ts.map +1 -1
- package/dist/session/types.d.ts +14 -1
- package/dist/session/types.d.ts.map +1 -1
- package/dist/session/types.js +7 -0
- package/dist/session/types.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Article, ProviderName } from '../../providers/base/types.js';
|
|
2
|
+
import { SessionFile, SessionSource } from '../../session/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Result of merging articles from multiple sessions.
|
|
5
|
+
*/
|
|
6
|
+
export interface MergeResult {
|
|
7
|
+
/** All unique articles after deduplication */
|
|
8
|
+
articles: Article[];
|
|
9
|
+
/** Articles grouped by provider */
|
|
10
|
+
byProvider: Map<ProviderName, Article[]>;
|
|
11
|
+
/** Total article count before deduplication */
|
|
12
|
+
totalBefore: number;
|
|
13
|
+
/** Total unique article count after deduplication */
|
|
14
|
+
totalAfter: number;
|
|
15
|
+
/** Number of duplicates removed */
|
|
16
|
+
duplicatesRemoved: number;
|
|
17
|
+
/** Per-session article counts (before dedup) */
|
|
18
|
+
perSession: Map<string, number>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Options for creating a merged session.
|
|
22
|
+
*/
|
|
23
|
+
export interface CreateMergedSessionOptions {
|
|
24
|
+
name: string;
|
|
25
|
+
sources: SessionSource[];
|
|
26
|
+
byProvider: Map<ProviderName, Article[]>;
|
|
27
|
+
totalRetrieved: number;
|
|
28
|
+
sessionsDir: string;
|
|
29
|
+
sourceSessionIds: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validation result for merge sources.
|
|
33
|
+
*/
|
|
34
|
+
export interface MergeValidationResult {
|
|
35
|
+
valid: boolean;
|
|
36
|
+
error?: string;
|
|
37
|
+
expandedCommand?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Output data for formatting merge results.
|
|
41
|
+
*/
|
|
42
|
+
export interface MergeOutputData {
|
|
43
|
+
sessionId: string;
|
|
44
|
+
totalBefore: number;
|
|
45
|
+
totalAfter: number;
|
|
46
|
+
duplicatesRemoved: number;
|
|
47
|
+
sources: Array<{
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
count: number;
|
|
51
|
+
}>;
|
|
52
|
+
byProvider: Map<string, number>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Merge articles from multiple sessions with identifier-based deduplication.
|
|
56
|
+
*
|
|
57
|
+
* When duplicates are found (same DOI, PMID, etc.), the article with
|
|
58
|
+
* richer metadata is kept.
|
|
59
|
+
*/
|
|
60
|
+
export declare function mergeArticles(sessionArticles: Map<string, Article[]>): MergeResult;
|
|
61
|
+
/**
|
|
62
|
+
* Copy source session provenance files to sources/ subdirectory.
|
|
63
|
+
* Copies session.yaml, query_common.yaml, and query text files.
|
|
64
|
+
*/
|
|
65
|
+
export declare function copySourceProvenance(sourceSessionId: string, sessionsDir: string, mergedSessionDir: string): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Validate that all source sessions are valid for merging.
|
|
68
|
+
*/
|
|
69
|
+
export declare function validateMergeSources(sessions: Map<string, SessionFile>): MergeValidationResult;
|
|
70
|
+
/**
|
|
71
|
+
* Create a merged session on disk.
|
|
72
|
+
*/
|
|
73
|
+
export declare function createMergedSession(options: CreateMergedSessionOptions): Promise<SessionFile>;
|
|
74
|
+
/**
|
|
75
|
+
* Format merge result as human-readable text.
|
|
76
|
+
*/
|
|
77
|
+
export declare function formatMergeOutput(data: MergeOutputData): string;
|
|
78
|
+
/**
|
|
79
|
+
* Format merge result as JSON.
|
|
80
|
+
*/
|
|
81
|
+
export declare function formatMergeJson(data: MergeOutputData): string;
|
|
82
|
+
//# sourceMappingURL=merge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/merge.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC3E,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAkB,MAAM,wBAAwB,CAAC;AAMzF;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,mCAAmC;IACnC,UAAU,EAAE,GAAG,CAAC,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC;IACzC,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,UAAU,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gDAAgD;IAChD,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,UAAU,EAAE,GAAG,CAAC,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5D,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAsBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,GACtC,WAAW,CAgEb;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,MAAM,EACnB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GACjC,qBAAqB,CA+BvB;AAYD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,WAAW,CAAC,CAuEtB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAqB/D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAS7D"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { mkdir, writeFile, copyFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { stringify } from "yaml";
|
|
5
|
+
import { isMergedSession } from "../../session/types.js";
|
|
6
|
+
import { sanitizeName } from "../../session/manager.js";
|
|
7
|
+
import { convertResultsToYaml } from "../../session/results-io.js";
|
|
8
|
+
import { getArticleKeys } from "./session-utils.js";
|
|
9
|
+
function countMetadataFields(article) {
|
|
10
|
+
let count = 0;
|
|
11
|
+
if (article.doi) count++;
|
|
12
|
+
if (article.pmid) count++;
|
|
13
|
+
if (article.arxivId) count++;
|
|
14
|
+
if (article.scopusId) count++;
|
|
15
|
+
if (article.ericId) count++;
|
|
16
|
+
if (article.abstract) count++;
|
|
17
|
+
if (article.publicationDate) count++;
|
|
18
|
+
if (article.journal) count++;
|
|
19
|
+
if (article.volume) count++;
|
|
20
|
+
if (article.issue) count++;
|
|
21
|
+
if (article.pages) count++;
|
|
22
|
+
if (article.authors.length > 0) count++;
|
|
23
|
+
return count;
|
|
24
|
+
}
|
|
25
|
+
function mergeArticles(sessionArticles) {
|
|
26
|
+
const keyToIndex = /* @__PURE__ */ new Map();
|
|
27
|
+
const unique = [];
|
|
28
|
+
let totalBefore = 0;
|
|
29
|
+
let duplicatesRemoved = 0;
|
|
30
|
+
const perSession = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const [sessionId, articles] of sessionArticles) {
|
|
32
|
+
perSession.set(sessionId, articles.length);
|
|
33
|
+
totalBefore += articles.length;
|
|
34
|
+
for (const article of articles) {
|
|
35
|
+
const keys = getArticleKeys(article);
|
|
36
|
+
if (keys.length === 0) {
|
|
37
|
+
unique.push(article);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
let existingIndex;
|
|
41
|
+
for (const key of keys) {
|
|
42
|
+
const idx = keyToIndex.get(key);
|
|
43
|
+
if (idx !== void 0) {
|
|
44
|
+
existingIndex = idx;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (existingIndex !== void 0) {
|
|
49
|
+
const existing = unique[existingIndex];
|
|
50
|
+
if (countMetadataFields(article) > countMetadataFields(existing)) {
|
|
51
|
+
unique[existingIndex] = article;
|
|
52
|
+
const newKeys = getArticleKeys(article);
|
|
53
|
+
for (const key of newKeys) {
|
|
54
|
+
keyToIndex.set(key, existingIndex);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
duplicatesRemoved++;
|
|
58
|
+
} else {
|
|
59
|
+
const index = unique.length;
|
|
60
|
+
unique.push(article);
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
keyToIndex.set(key, index);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const article of unique) {
|
|
69
|
+
const existing = byProvider.get(article.source) ?? [];
|
|
70
|
+
existing.push(article);
|
|
71
|
+
byProvider.set(article.source, existing);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
articles: unique,
|
|
75
|
+
byProvider,
|
|
76
|
+
totalBefore,
|
|
77
|
+
totalAfter: unique.length,
|
|
78
|
+
duplicatesRemoved,
|
|
79
|
+
perSession
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function copySourceProvenance(sourceSessionId, sessionsDir, mergedSessionDir) {
|
|
83
|
+
const sourceDir = join(sessionsDir, sourceSessionId);
|
|
84
|
+
const targetDir = join(mergedSessionDir, "sources", sourceSessionId);
|
|
85
|
+
await mkdir(targetDir, { recursive: true });
|
|
86
|
+
for (const file of ["session.yaml", "query_common.yaml"]) {
|
|
87
|
+
try {
|
|
88
|
+
await copyFile(join(sourceDir, file), join(targetDir, file));
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const entries = await readdir(sourceDir);
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (entry.startsWith("query_") && entry.endsWith(".txt")) {
|
|
95
|
+
await copyFile(join(sourceDir, entry), join(targetDir, entry));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function validateMergeSources(sessions) {
|
|
100
|
+
for (const [sessionId, session] of sessions) {
|
|
101
|
+
if (isMergedSession(session)) {
|
|
102
|
+
const originalSources = session.sources?.map((s) => s.id) ?? [];
|
|
103
|
+
const otherIds = [...sessions.keys()].filter((id) => id !== sessionId);
|
|
104
|
+
const allSources = [.../* @__PURE__ */ new Set([...originalSources, ...otherIds])];
|
|
105
|
+
const expandedCommand = `search-hub merge ${allSources.join(" ")}`;
|
|
106
|
+
return {
|
|
107
|
+
valid: false,
|
|
108
|
+
error: `Session '${sessionId}' is a merged session (sources: ${originalSources.join(", ")}).
|
|
109
|
+
Merge the original sources directly:
|
|
110
|
+
${expandedCommand}`,
|
|
111
|
+
expandedCommand
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const [sessionId, session] of sessions) {
|
|
116
|
+
if (session.summary.status !== "completed") {
|
|
117
|
+
return {
|
|
118
|
+
valid: false,
|
|
119
|
+
error: `Session '${sessionId}' is not completed (status: ${session.summary.status}). Only completed sessions can be merged.`
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { valid: true };
|
|
124
|
+
}
|
|
125
|
+
function generateMergedSessionId(name, sourceIds) {
|
|
126
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
|
|
127
|
+
const sanitized = sanitizeName(name);
|
|
128
|
+
const hash = createHash("sha256").update(sourceIds.join(",")).digest("hex").slice(0, 6);
|
|
129
|
+
return `${date}_${sanitized}_${hash}`;
|
|
130
|
+
}
|
|
131
|
+
async function createMergedSession(options) {
|
|
132
|
+
const { name, sources, byProvider, totalRetrieved, sessionsDir, sourceSessionIds } = options;
|
|
133
|
+
const id = generateMergedSessionId(name, sourceSessionIds);
|
|
134
|
+
const sessionDir = join(sessionsDir, id);
|
|
135
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
136
|
+
await mkdir(sessionDir, { recursive: true });
|
|
137
|
+
const databases = {};
|
|
138
|
+
for (const [provider, articles] of byProvider) {
|
|
139
|
+
const jsonlFilename = `${provider}_results.jsonl`;
|
|
140
|
+
const yamlFilename = `${provider}_results.yaml`;
|
|
141
|
+
const jsonlPath = join(sessionDir, jsonlFilename);
|
|
142
|
+
const jsonlContent = articles.map((a) => JSON.stringify(a)).join("\n") + "\n";
|
|
143
|
+
await writeFile(jsonlPath, jsonlContent, "utf-8");
|
|
144
|
+
const yamlPath = join(sessionDir, yamlFilename);
|
|
145
|
+
await convertResultsToYaml(jsonlPath, yamlPath, {
|
|
146
|
+
provider,
|
|
147
|
+
queryName: name
|
|
148
|
+
});
|
|
149
|
+
databases[provider] = {
|
|
150
|
+
status: "completed",
|
|
151
|
+
retrievedCount: articles.length,
|
|
152
|
+
files: {
|
|
153
|
+
query: "",
|
|
154
|
+
results: jsonlFilename,
|
|
155
|
+
resultsYaml: yamlFilename
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const sessionFile = {
|
|
160
|
+
version: 1,
|
|
161
|
+
id,
|
|
162
|
+
name,
|
|
163
|
+
type: "merge",
|
|
164
|
+
createdAt: now,
|
|
165
|
+
updatedAt: now,
|
|
166
|
+
sources,
|
|
167
|
+
databases,
|
|
168
|
+
summary: {
|
|
169
|
+
totalHits: 0,
|
|
170
|
+
totalRetrieved,
|
|
171
|
+
status: "completed"
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
await writeFile(
|
|
175
|
+
join(sessionDir, "session.yaml"),
|
|
176
|
+
stringify(sessionFile),
|
|
177
|
+
"utf-8"
|
|
178
|
+
);
|
|
179
|
+
for (const sourceId of sourceSessionIds) {
|
|
180
|
+
await copySourceProvenance(sourceId, sessionsDir, sessionDir);
|
|
181
|
+
}
|
|
182
|
+
return sessionFile;
|
|
183
|
+
}
|
|
184
|
+
function formatMergeOutput(data) {
|
|
185
|
+
const lines = [];
|
|
186
|
+
lines.push(`Merged session: ${data.sessionId}`);
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push("Sources:");
|
|
189
|
+
for (const source of data.sources) {
|
|
190
|
+
lines.push(` ${source.id} (${source.name}): ${source.count} articles`);
|
|
191
|
+
}
|
|
192
|
+
lines.push("");
|
|
193
|
+
lines.push(`Total articles: ${data.totalBefore} → ${data.totalAfter} unique (${data.duplicatesRemoved} duplicates removed)`);
|
|
194
|
+
if (data.byProvider.size > 0) {
|
|
195
|
+
lines.push("");
|
|
196
|
+
lines.push("By database:");
|
|
197
|
+
for (const [provider, count] of data.byProvider) {
|
|
198
|
+
lines.push(` ${provider}: ${count} articles`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
|
203
|
+
function formatMergeJson(data) {
|
|
204
|
+
return JSON.stringify({
|
|
205
|
+
sessionId: data.sessionId,
|
|
206
|
+
totalBefore: data.totalBefore,
|
|
207
|
+
totalAfter: data.totalAfter,
|
|
208
|
+
duplicatesRemoved: data.duplicatesRemoved,
|
|
209
|
+
sources: data.sources,
|
|
210
|
+
byProvider: Object.fromEntries(data.byProvider)
|
|
211
|
+
}, null, 2);
|
|
212
|
+
}
|
|
213
|
+
export {
|
|
214
|
+
copySourceProvenance,
|
|
215
|
+
createMergedSession,
|
|
216
|
+
formatMergeJson,
|
|
217
|
+
formatMergeOutput,
|
|
218
|
+
mergeArticles,
|
|
219
|
+
validateMergeSources
|
|
220
|
+
};
|
|
221
|
+
//# sourceMappingURL=merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.js","sources":["../../../src/cli/commands/merge.ts"],"sourcesContent":["import { mkdir, writeFile, readFile, readdir, copyFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { createHash } from 'node:crypto';\nimport { stringify as stringifyYaml } from 'yaml';\nimport type { Article, ProviderName } from '../../providers/base/types.js';\nimport type { SessionFile, SessionSource, DatabaseStatus } from '../../session/types.js';\nimport { isMergedSession } from '../../session/types.js';\nimport { sanitizeName } from '../../session/manager.js';\nimport { convertResultsToYaml } from '../../session/results-io.js';\nimport { getArticleKeys } from './session-utils.js';\n\n/**\n * Result of merging articles from multiple sessions.\n */\nexport interface MergeResult {\n /** All unique articles after deduplication */\n articles: Article[];\n /** Articles grouped by provider */\n byProvider: Map<ProviderName, Article[]>;\n /** Total article count before deduplication */\n totalBefore: number;\n /** Total unique article count after deduplication */\n totalAfter: number;\n /** Number of duplicates removed */\n duplicatesRemoved: number;\n /** Per-session article counts (before dedup) */\n perSession: Map<string, number>;\n}\n\n/**\n * Options for creating a merged session.\n */\nexport interface CreateMergedSessionOptions {\n name: string;\n sources: SessionSource[];\n byProvider: Map<ProviderName, Article[]>;\n totalRetrieved: number;\n sessionsDir: string;\n sourceSessionIds: string[];\n}\n\n/**\n * Validation result for merge sources.\n */\nexport interface MergeValidationResult {\n valid: boolean;\n error?: string;\n expandedCommand?: string;\n}\n\n/**\n * Output data for formatting merge results.\n */\nexport interface MergeOutputData {\n sessionId: string;\n totalBefore: number;\n totalAfter: number;\n duplicatesRemoved: number;\n sources: Array<{ id: string; name: string; count: number }>;\n byProvider: Map<string, number>;\n}\n\n/**\n * Count metadata fields for comparing article richness.\n */\nfunction countMetadataFields(article: Article): number {\n let count = 0;\n if (article.doi) count++;\n if (article.pmid) count++;\n if (article.arxivId) count++;\n if (article.scopusId) count++;\n if (article.ericId) count++;\n if (article.abstract) count++;\n if (article.publicationDate) count++;\n if (article.journal) count++;\n if (article.volume) count++;\n if (article.issue) count++;\n if (article.pages) count++;\n if (article.authors.length > 0) count++;\n return count;\n}\n\n/**\n * Merge articles from multiple sessions with identifier-based deduplication.\n *\n * When duplicates are found (same DOI, PMID, etc.), the article with\n * richer metadata is kept.\n */\nexport function mergeArticles(\n sessionArticles: Map<string, Article[]>,\n): MergeResult {\n const keyToIndex = new Map<string, number>();\n const unique: Article[] = [];\n let totalBefore = 0;\n let duplicatesRemoved = 0;\n const perSession = new Map<string, number>();\n\n for (const [sessionId, articles] of sessionArticles) {\n perSession.set(sessionId, articles.length);\n totalBefore += articles.length;\n\n for (const article of articles) {\n const keys = getArticleKeys(article);\n\n if (keys.length === 0) {\n unique.push(article);\n continue;\n }\n\n let existingIndex: number | undefined;\n for (const key of keys) {\n const idx = keyToIndex.get(key);\n if (idx !== undefined) {\n existingIndex = idx;\n break;\n }\n }\n\n if (existingIndex !== undefined) {\n const existing = unique[existingIndex]!;\n if (countMetadataFields(article) > countMetadataFields(existing)) {\n unique[existingIndex] = article;\n const newKeys = getArticleKeys(article);\n for (const key of newKeys) {\n keyToIndex.set(key, existingIndex);\n }\n }\n duplicatesRemoved++;\n } else {\n const index = unique.length;\n unique.push(article);\n for (const key of keys) {\n keyToIndex.set(key, index);\n }\n }\n }\n }\n\n // Group by provider\n const byProvider = new Map<ProviderName, Article[]>();\n for (const article of unique) {\n const existing = byProvider.get(article.source) ?? [];\n existing.push(article);\n byProvider.set(article.source, existing);\n }\n\n return {\n articles: unique,\n byProvider,\n totalBefore,\n totalAfter: unique.length,\n duplicatesRemoved,\n perSession,\n };\n}\n\n/**\n * Copy source session provenance files to sources/ subdirectory.\n * Copies session.yaml, query_common.yaml, and query text files.\n */\nexport async function copySourceProvenance(\n sourceSessionId: string,\n sessionsDir: string,\n mergedSessionDir: string,\n): Promise<void> {\n const sourceDir = join(sessionsDir, sourceSessionId);\n const targetDir = join(mergedSessionDir, 'sources', sourceSessionId);\n await mkdir(targetDir, { recursive: true });\n\n // Copy session.yaml and query_common.yaml\n for (const file of ['session.yaml', 'query_common.yaml']) {\n try {\n await copyFile(join(sourceDir, file), join(targetDir, file));\n } catch {\n // Skip if file doesn't exist\n }\n }\n\n // Copy query text files (query_*.txt)\n const entries = await readdir(sourceDir);\n for (const entry of entries) {\n if (entry.startsWith('query_') && entry.endsWith('.txt')) {\n await copyFile(join(sourceDir, entry), join(targetDir, entry));\n }\n }\n}\n\n/**\n * Validate that all source sessions are valid for merging.\n */\nexport function validateMergeSources(\n sessions: Map<string, SessionFile>,\n): MergeValidationResult {\n // Check for merged sessions as sources\n for (const [sessionId, session] of sessions) {\n if (isMergedSession(session)) {\n // Collect original sources from the merged session\n const originalSources = session.sources?.map((s) => s.id) ?? [];\n // Collect non-merged session IDs\n const otherIds = [...sessions.keys()].filter((id) => id !== sessionId);\n // Build expanded command with original sources + other sessions\n const allSources = [...new Set([...originalSources, ...otherIds])];\n const expandedCommand = `search-hub merge ${allSources.join(' ')}`;\n\n return {\n valid: false,\n error: `Session '${sessionId}' is a merged session (sources: ${originalSources.join(', ')}).\\n Merge the original sources directly:\\n ${expandedCommand}`,\n expandedCommand,\n };\n }\n }\n\n // Check that all sessions are completed\n for (const [sessionId, session] of sessions) {\n if (session.summary.status !== 'completed') {\n return {\n valid: false,\n error: `Session '${sessionId}' is not completed (status: ${session.summary.status}). Only completed sessions can be merged.`,\n };\n }\n }\n\n return { valid: true };\n}\n\n/**\n * Generate a session ID for a merged session.\n */\nfunction generateMergedSessionId(name: string, sourceIds: string[]): string {\n const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');\n const sanitized = sanitizeName(name);\n const hash = createHash('sha256').update(sourceIds.join(',')).digest('hex').slice(0, 6);\n return `${date}_${sanitized}_${hash}`;\n}\n\n/**\n * Create a merged session on disk.\n */\nexport async function createMergedSession(\n options: CreateMergedSessionOptions,\n): Promise<SessionFile> {\n const { name, sources, byProvider, totalRetrieved, sessionsDir, sourceSessionIds } = options;\n\n const id = generateMergedSessionId(name, sourceSessionIds);\n const sessionDir = join(sessionsDir, id);\n const now = new Date().toISOString();\n\n await mkdir(sessionDir, { recursive: true });\n\n // Build database statuses and write result files\n const databases: Partial<Record<ProviderName, DatabaseStatus>> = {};\n\n for (const [provider, articles] of byProvider) {\n const jsonlFilename = `${provider}_results.jsonl`;\n const yamlFilename = `${provider}_results.yaml`;\n const jsonlPath = join(sessionDir, jsonlFilename);\n\n // Write JSONL results\n const jsonlContent = articles\n .map((a) => JSON.stringify(a))\n .join('\\n') + '\\n';\n await writeFile(jsonlPath, jsonlContent, 'utf-8');\n\n // Convert to YAML\n const yamlPath = join(sessionDir, yamlFilename);\n await convertResultsToYaml(jsonlPath, yamlPath, {\n provider,\n queryName: name,\n });\n\n databases[provider] = {\n status: 'completed',\n retrievedCount: articles.length,\n files: {\n query: '',\n results: jsonlFilename,\n resultsYaml: yamlFilename,\n },\n };\n }\n\n // Create session file\n const sessionFile: SessionFile = {\n version: 1,\n id,\n name,\n type: 'merge',\n createdAt: now,\n updatedAt: now,\n sources,\n databases,\n summary: {\n totalHits: 0,\n totalRetrieved: totalRetrieved,\n status: 'completed',\n },\n };\n\n // Write session.yaml\n await writeFile(\n join(sessionDir, 'session.yaml'),\n stringifyYaml(sessionFile),\n 'utf-8',\n );\n\n // Copy provenance from source sessions\n for (const sourceId of sourceSessionIds) {\n await copySourceProvenance(sourceId, sessionsDir, sessionDir);\n }\n\n return sessionFile;\n}\n\n/**\n * Format merge result as human-readable text.\n */\nexport function formatMergeOutput(data: MergeOutputData): string {\n const lines: string[] = [];\n\n lines.push(`Merged session: ${data.sessionId}`);\n lines.push('');\n lines.push('Sources:');\n for (const source of data.sources) {\n lines.push(` ${source.id} (${source.name}): ${source.count} articles`);\n }\n lines.push('');\n lines.push(`Total articles: ${data.totalBefore} → ${data.totalAfter} unique (${data.duplicatesRemoved} duplicates removed)`);\n\n if (data.byProvider.size > 0) {\n lines.push('');\n lines.push('By database:');\n for (const [provider, count] of data.byProvider) {\n lines.push(` ${provider}: ${count} articles`);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format merge result as JSON.\n */\nexport function formatMergeJson(data: MergeOutputData): string {\n return JSON.stringify({\n sessionId: data.sessionId,\n totalBefore: data.totalBefore,\n totalAfter: data.totalAfter,\n duplicatesRemoved: data.duplicatesRemoved,\n sources: data.sources,\n byProvider: Object.fromEntries(data.byProvider),\n }, null, 2);\n}\n"],"names":["stringifyYaml"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resume.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/resume.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"resume.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/resume.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAI9E,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACnC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kBAAkB,GAC1B,oBAAoB,CActB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,gBAAgB,CASnF;AAED,wBAAsB,+BAA+B,CACnD,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;IAAE,SAAS,CAAC,EAAE,YAAY,EAAE,GAAG,SAAS,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAAE,GACrF,OAAO,CAAC,YAAY,CAAC,CAmCvB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadSession, getResumableProviders } from "../../session/manager.js";
|
|
2
|
+
import { isMergedSession } from "../../session/types.js";
|
|
2
3
|
import { parseProviderNames } from "../utils/validation.js";
|
|
3
4
|
function parseResumeOptions(sessionId, options) {
|
|
4
5
|
const result = {
|
|
@@ -24,6 +25,12 @@ function validateResumeInput(options) {
|
|
|
24
25
|
async function getResumableProvidersForCommand(sessionId, sessionsDir, options) {
|
|
25
26
|
try {
|
|
26
27
|
const session = await loadSession(sessionId, sessionsDir);
|
|
28
|
+
if (isMergedSession(session)) {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
error: `Cannot resume merged session '${sessionId}'. Merged sessions are created from other sessions and cannot be resumed.`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
27
34
|
let resumable = getResumableProviders(session);
|
|
28
35
|
if (options.providers && options.providers.length > 0) {
|
|
29
36
|
resumable = resumable.filter((r) => options.providers.includes(r.provider));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resume.js","sources":["../../../src/cli/commands/resume.ts"],"sourcesContent":["import { loadSession, getResumableProviders } from '../../session/manager.js';\nimport type { ProviderName, ResumableProvider } from '../../session/types.js';\nimport { parseProviderNames } from '../utils/validation.js';\n\nexport interface ResumeCommandOptions {\n sessionId: string;\n providers?: ProviderName[];\n retryFailed?: boolean;\n}\n\nexport interface CommandLineOptions {\n db?: string | undefined;\n retryFailed?: boolean | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\nexport interface ResumeResult {\n success: boolean;\n providers?: ResumableProvider[];\n error?: string;\n}\n\nexport function parseResumeOptions(\n sessionId: string,\n options: CommandLineOptions\n): ResumeCommandOptions {\n const result: ResumeCommandOptions = {\n sessionId,\n };\n\n if (options.db) {\n result.providers = parseProviderNames(options.db);\n }\n\n if (options.retryFailed) {\n result.retryFailed = true;\n }\n\n return result;\n}\n\nexport function validateResumeInput(options: ResumeCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n return { valid: true };\n}\n\nexport async function getResumableProvidersForCommand(\n sessionId: string,\n sessionsDir: string,\n options: { providers?: ProviderName[] | undefined; retryFailed?: boolean | undefined }\n): Promise<ResumeResult> {\n try {\n const session = await loadSession(sessionId, sessionsDir);\n let resumable = getResumableProviders(session);\n\n // Filter by specific providers if requested\n if (options.providers && options.providers.length > 0) {\n resumable = resumable.filter((r) => options.providers!.includes(r.provider));\n }\n\n // Filter to only retry strategies if retryFailed is true\n if (options.retryFailed) {\n resumable = resumable.filter((r) => r.strategy === 'retry');\n }\n\n return {\n success: true,\n providers: resumable,\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"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"resume.js","sources":["../../../src/cli/commands/resume.ts"],"sourcesContent":["import { loadSession, getResumableProviders } from '../../session/manager.js';\nimport type { ProviderName, ResumableProvider } from '../../session/types.js';\nimport { isMergedSession } from '../../session/types.js';\nimport { parseProviderNames } from '../utils/validation.js';\n\nexport interface ResumeCommandOptions {\n sessionId: string;\n providers?: ProviderName[];\n retryFailed?: boolean;\n}\n\nexport interface CommandLineOptions {\n db?: string | undefined;\n retryFailed?: boolean | undefined;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n error?: string;\n}\n\nexport interface ResumeResult {\n success: boolean;\n providers?: ResumableProvider[];\n error?: string;\n}\n\nexport function parseResumeOptions(\n sessionId: string,\n options: CommandLineOptions\n): ResumeCommandOptions {\n const result: ResumeCommandOptions = {\n sessionId,\n };\n\n if (options.db) {\n result.providers = parseProviderNames(options.db);\n }\n\n if (options.retryFailed) {\n result.retryFailed = true;\n }\n\n return result;\n}\n\nexport function validateResumeInput(options: ResumeCommandOptions): ValidationResult {\n if (!options.sessionId || options.sessionId.trim() === '') {\n return {\n valid: false,\n error: 'A session ID is required',\n };\n }\n\n return { valid: true };\n}\n\nexport async function getResumableProvidersForCommand(\n sessionId: string,\n sessionsDir: string,\n options: { providers?: ProviderName[] | undefined; retryFailed?: boolean | undefined }\n): Promise<ResumeResult> {\n try {\n const session = await loadSession(sessionId, sessionsDir);\n\n // Reject merged sessions\n if (isMergedSession(session)) {\n return {\n success: false,\n error: `Cannot resume merged session '${sessionId}'. Merged sessions are created from other sessions and cannot be resumed.`,\n };\n }\n\n let resumable = getResumableProviders(session);\n\n // Filter by specific providers if requested\n if (options.providers && options.providers.length > 0) {\n resumable = resumable.filter((r) => options.providers!.includes(r.provider));\n }\n\n // Filter to only retry strategies if retryFailed is true\n if (options.retryFailed) {\n resumable = resumable.filter((r) => r.strategy === 'retry');\n }\n\n return {\n success: true,\n providers: resumable,\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"],"names":[],"mappings":";;;AA2BO,SAAS,mBACd,WACA,SACsB;AACtB,QAAM,SAA+B;AAAA,IACnC;AAAA,EAAA;AAGF,MAAI,QAAQ,IAAI;AACd,WAAO,YAAY,mBAAmB,QAAQ,EAAE;AAAA,EAClD;AAEA,MAAI,QAAQ,aAAa;AACvB,WAAO,cAAc;AAAA,EACvB;AAEA,SAAO;AACT;AAEO,SAAS,oBAAoB,SAAiD;AACnF,MAAI,CAAC,QAAQ,aAAa,QAAQ,UAAU,KAAA,MAAW,IAAI;AACzD,WAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO;AAAA,IAAA;AAAA,EAEX;AAEA,SAAO,EAAE,OAAO,KAAA;AAClB;AAEA,eAAsB,gCACpB,WACA,aACA,SACuB;AACvB,MAAI;AACF,UAAM,UAAU,MAAM,YAAY,WAAW,WAAW;AAGxD,QAAI,gBAAgB,OAAO,GAAG;AAC5B,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO,iCAAiC,SAAS;AAAA,MAAA;AAAA,IAErD;AAEA,QAAI,YAAY,sBAAsB,OAAO;AAG7C,QAAI,QAAQ,aAAa,QAAQ,UAAU,SAAS,GAAG;AACrD,kBAAY,UAAU,OAAO,CAAC,MAAM,QAAQ,UAAW,SAAS,EAAE,QAAQ,CAAC;AAAA,IAC7E;AAGA,QAAI,QAAQ,aAAa;AACvB,kBAAY,UAAU,OAAO,CAAC,MAAM,EAAE,aAAa,OAAO;AAAA,IAC5D;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,WAAW;AAAA,IAAA;AAAA,EAEf,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AACF;"}
|
|
@@ -43,7 +43,7 @@ async function getSessionDetails(sessionId, sessionsDir) {
|
|
|
43
43
|
status: session.summary.status,
|
|
44
44
|
createdAt: session.createdAt,
|
|
45
45
|
updatedAt: session.updatedAt,
|
|
46
|
-
queryFile: session.query
|
|
46
|
+
queryFile: session.query?.file ?? "",
|
|
47
47
|
totalHits: session.summary.totalHits,
|
|
48
48
|
totalRetrieved: session.summary.totalRetrieved,
|
|
49
49
|
databases
|
|
@@ -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 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
|
+
{"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":";;;;;AAsDA,eAAsB,uBACpB,aACA,SAC4B;AAC5B,QAAM,YAAY,MAAM,aAAa,WAAW;AAEhD,QAAM,WAAW,QAAQ,MACrB,YACA,UAAU,OAAO,CAAC,MAAM,EAAE,WAAW,WAAW;AAEpD,QAAM,WAAW,SAAS,IAAI,CAAC,OAAO;AAAA,IACpC,IAAI,EAAE;AAAA,IACN,MAAM,EAAE;AAAA,IACR,QAAQ,EAAE;AAAA,IACV,WAAW,EAAE;AAAA,IACb,UAAU,GAAG,EAAE,cAAc,IAAI,EAAE,SAAS;AAAA,EAAA,EAC5C;AAEF,SAAO;AAAA,IACL;AAAA,IACA,YAAY,UAAU;AAAA,IACtB,eAAe,SAAS;AAAA,IACxB,YAAY,QAAQ;AAAA,EAAA;AAExB;AAEA,eAAsB,kBACpB,WACA,aACyE;AACzE,MAAI;AACF,UAAM,UAAU,MAAM,YAAY,WAAW,WAAW;AAExD,UAAM,YAA+B,CAAA;AACrC,eAAW,CAAC,UAAU,QAAQ,KAAK,OAAO,QAAQ,QAAQ,SAAS,GAAG;AACpE,UAAI,CAAC,SAAU;AACf,YAAM,WAA4B;AAAA,QAChC;AAAA,QACA,QAAQ,SAAS;AAAA,QACjB,WAAW,SAAS,aAAa;AAAA,QACjC,gBAAgB,SAAS,kBAAkB;AAAA,MAAA;AAE7C,UAAI,SAAS,OAAO,SAAS;AAC3B,iBAAS,QAAQ,SAAS,MAAM;AAAA,MAClC;AACA,gBAAU,KAAK,QAAQ;AAAA,IACzB;AAEA,UAAM,iBAAiC;AAAA,MACrC,IAAI,QAAQ;AAAA,MACZ,MAAM,QAAQ;AAAA,MACd,QAAQ,QAAQ,QAAQ;AAAA,MACxB,WAAW,QAAQ;AAAA,MACnB,WAAW,QAAQ;AAAA,MACnB,WAAW,QAAQ,OAAO,QAAQ;AAAA,MAClC,WAAW,QAAQ,QAAQ;AAAA,MAC3B,gBAAgB,QAAQ,QAAQ;AAAA,MAChC;AAAA,IAAA;AAEF,QAAI,QAAQ,aAAa;AACvB,qBAAe,cAAc,QAAQ;AAAA,IACvC;AAGA,QAAI;AACF,YAAM,aAAa,KAAK,aAAa,SAAS;AAC9C,YAAM,QAAQ,MAAM,UAAU,UAAU;AACxC,UAAI,MAAM,SAAS,GAAG;AACpB,uBAAe,QAAQ;AAAA,MACzB;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS;AAAA,IAAA;AAAA,EAEb,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AACF;AAEA,eAAsB,0BACpB,WACA,aACA,SACgE;AAChE,QAAM,WAA8D,CAAA;AACpE,QAAM,aAAa,KAAK,aAAa,SAAS;AAE9C,aAAW,CAAC,YAAY,KAAK,OAAO,QAAQ,QAAQ,SAAS,GAAG;AAC9D,UAAM,mBAAmB,MAAM,YAAY,YAAY,YAA4B;AACnF,aAAS,KAAK,GAAG,gBAAgB;AAAA,EACnC;AAEA,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO,EAAE,gBAAgB,GAAG,mBAAmB,EAAA;AAAA,EACjD;AAEA,QAAM,SAAS,oBAAoB,QAAQ;AAC3C,SAAO;AAAA,IACL,gBAAgB,OAAO,SAAS;AAAA,IAChC,mBAAmB,OAAO;AAAA,EAAA;AAE9B;AAEO,SAAS,kBACd,QACA,SACQ;AACR,MAAI,QAAQ,MAAM;AAChB,WAAO,KAAK,UAAU,OAAO,UAAU,MAAM,CAAC;AAAA,EAChD;AAEA,MAAI,OAAO,SAAS,WAAW,GAAG;AAChC,QAAI,OAAO,eAAe,GAAG;AAC3B,aAAO;AAAA,IACT;AAEA,UAAM,iBAAiB,OAAO,aAAa,OAAO;AAClD,WAAO,uBAAuB,cAAc,qBAAqB,mBAAmB,IAAI,KAAK,GAAG;AAAA,EAClG;AAEA,QAAM,SAAS,GAAG,KAAK,OAAO,EAAE,CAAC,IAAI,OAAO,OAAO,EAAE,CAAC,IAAI,SAAS,OAAO,EAAE,CAAC,IAAI,WAAW,OAAO,EAAE,CAAC;AACtG,QAAM,YAAY,IAAI,OAAO,GAAG;AAEhC,QAAM,OAAO,OAAO,SAAS,IAAI,CAAC,MAAM;AACtC,UAAM,OAAO,IAAI,KAAK,EAAE,SAAS,EAAE,mBAAA;AACnC,WAAO,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,OAAO,EAAE,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,CAAC,IAAI,IAAI;AAAA,EACxG,CAAC;AAED,QAAM,QAAQ,CAAC,QAAQ,WAAW,GAAG,IAAI;AAGzC,MAAI,CAAC,OAAO,cAAc,OAAO,aAAa,OAAO,eAAe;AAClE,UAAM,cAAc,OAAO,aAAa,OAAO;AAC/C,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,IAAI,WAAW,qBAAqB,gBAAgB,IAAI,KAAK,GAAG,6BAA6B;AAAA,EAC1G;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEO,SAAS,qBACd,SACA,SACQ;AACR,MAAI,QAAQ,MAAM;AAChB,WAAO,KAAK,UAAU,SAAS,MAAM,CAAC;AAAA,EACxC;AAEA,QAAM,QAAkB,CAAA;AAExB,QAAM,KAAK,YAAY,QAAQ,IAAI,EAAE;AACrC,QAAM,KAAK,OAAO,QAAQ,EAAE,EAAE;AAC9B,MAAI,QAAQ,aAAa;AACvB,UAAM,KAAK,gBAAgB,QAAQ,WAAW,EAAE;AAAA,EAClD;AACA,QAAM,KAAK,WAAW,QAAQ,MAAM,EAAE;AACtC,QAAM,KAAK,eAAe,QAAQ,SAAS,EAAE;AAC7C,QAAM,KAAK,YAAY,IAAI,KAAK,QAAQ,SAAS,EAAE,eAAA,CAAgB,EAAE;AACrE,QAAM,KAAK,YAAY,IAAI,KAAK,QAAQ,SAAS,EAAE,eAAA,CAAgB,EAAE;AACrE,QAAM,KAAK,EAAE;AACb,MAAI,QAAQ,sBAAsB,UAAa,QAAQ,oBAAoB,GAAG;AAC5E,UAAM,KAAK,UAAU,QAAQ,cAAc,UAAU,QAAQ,cAAc,YAAY,QAAQ,iBAAiB,cAAc;AAAA,EAChI,OAAO;AACL,UAAM,KAAK,UAAU,QAAQ,cAAc,IAAI,QAAQ,SAAS,UAAU;AAAA,EAC5E;AACA,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,YAAY;AAEvB,aAAW,MAAM,QAAQ,WAAW;AAClC,UAAM,aAAa,cAAc,GAAG,MAAM;AAC1C,QAAI,OAAO,KAAK,UAAU,IAAI,GAAG,SAAS,OAAO,EAAE,CAAC,IAAI,GAAG,OAAO,OAAO,EAAE,CAAC,IAAI,GAAG,cAAc,IAAI,GAAG,SAAS;AACjH,QAAI,GAAG,OAAO;AACZ,cAAQ,KAAK,GAAG,KAAK;AAAA,IACvB;AACA,UAAM,KAAK,IAAI;AAAA,EACjB;AAGA,MAAI,QAAQ,SAAS,QAAQ,MAAM,SAAS,GAAG;AAC7C,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,QAAQ;AACnB,UAAM,KAAK,gBAAgB,QAAQ,KAAK,CAAC;AAAA,EAC3C;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,cAAc,QAAwB;AAC7C,UAAQ,QAAA;AAAA,IACN,KAAK;AACH,aAAO;AAAA;AAAA,IACT,KAAK;AACH,aAAO;AAAA;AAAA,IACT,KAAK;AACH,aAAO;AAAA;AAAA,IACT,KAAK;AACH,aAAO;AAAA;AAAA,IACT;AACE,aAAO;AAAA,EAAA;AAEb;"}
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqKpC;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CA0tEvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
|
package/dist/cli/index.js
CHANGED
|
@@ -23,11 +23,12 @@ import { computeSummary, formatSummaryJson, formatSummary } from "./commands/sum
|
|
|
23
23
|
import { parseResultsOptions, validateResultsInput, formatResultsJson, formatResultsList } from "./commands/results.js";
|
|
24
24
|
import { loadNotes, formatAllSessionNotes, formatNotesList, addNote, addAssessment } from "./commands/notes.js";
|
|
25
25
|
import { computeDiff, computeQueryDiff, formatDiffJson, formatDiff } from "./commands/diff.js";
|
|
26
|
+
import { validateMergeSources, mergeArticles, formatMergeJson, formatMergeOutput, createMergedSession } from "./commands/merge.js";
|
|
26
27
|
import { executeReviewInit } from "./commands/review/init.js";
|
|
27
28
|
import { executeReviewStatus, formatStatusOutput } from "./commands/review/status.js";
|
|
28
29
|
import { executeReviewList, formatListOutput } from "./commands/review/list.js";
|
|
29
30
|
import { executeReviewExtract } from "./commands/review/extract.js";
|
|
30
|
-
import { executeReviewMerge, formatMergeOutput } from "./commands/review/merge.js";
|
|
31
|
+
import { executeReviewMerge, formatMergeOutput as formatMergeOutput$1 } from "./commands/review/merge.js";
|
|
31
32
|
import { executeReviewMark } from "./commands/review/mark.js";
|
|
32
33
|
import { executeReviewExport, formatExportOutput } from "./commands/review/export.js";
|
|
33
34
|
import { executeReviewFinalize, formatFinalizeOutput } from "./commands/review/finalize.js";
|
|
@@ -295,7 +296,13 @@ Examples:
|
|
|
295
296
|
$ search-hub search --db pubmed --query "diabetes[tiab]" # Direct query
|
|
296
297
|
$ search-hub search ./query.yaml --dry-run # Preview translations
|
|
297
298
|
$ search-hub search ./query.yaml --count-only # Get hit counts only
|
|
298
|
-
$ search-hub search ./query.yaml --max-results 100 # Limit results
|
|
299
|
+
$ search-hub search ./query.yaml --max-results 100 # Limit results
|
|
300
|
+
|
|
301
|
+
Query features (use "query init" to see full template):
|
|
302
|
+
filters: year_from, year_to, language, publication_types
|
|
303
|
+
exclude: NOT terms per block (terms.exclude)
|
|
304
|
+
mesh/eric: controlled vocabulary (terms.mesh, terms.eric)
|
|
305
|
+
overrides: per-database settings (pubmed, scopus, eric, arxiv)`).action(
|
|
299
306
|
async (queryFile, options) => {
|
|
300
307
|
const globalOpts = program.opts();
|
|
301
308
|
try {
|
|
@@ -967,6 +974,135 @@ Query Refinement Workflow:
|
|
|
967
974
|
} else {
|
|
968
975
|
if (!globalOpts.quiet) {
|
|
969
976
|
console.log(formatDiff(diff, sessionId1, sessionId2, showFilter, formatOptions));
|
|
977
|
+
const suggestion = formatSuggestion(getSuggestion({
|
|
978
|
+
command: "diff",
|
|
979
|
+
sessionId: sessionId2,
|
|
980
|
+
diffSession1Id: sessionId1,
|
|
981
|
+
diffAddedCount: diff.added.length,
|
|
982
|
+
diffRemovedCount: diff.removed.length
|
|
983
|
+
}));
|
|
984
|
+
if (suggestion) {
|
|
985
|
+
console.log(suggestion);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
990
|
+
} catch (error) {
|
|
991
|
+
if (!globalOpts.quiet) {
|
|
992
|
+
console.error(
|
|
993
|
+
"Error:",
|
|
994
|
+
error instanceof Error ? error.message : error
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
process.exitCode = EXIT_CODES.SESSION_ERROR;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
);
|
|
1001
|
+
program.command("merge").description("Merge results from multiple search sessions").argument("<session-ids...>", "two or more session IDs to merge").option("--name <string>", "name for merged session").option("--dry-run", "show what would be merged without creating session").option("--json", "output as JSON").addHelpText("after", `
|
|
1002
|
+
Examples:
|
|
1003
|
+
$ search-hub merge session-v4 session-v9 # Merge two sessions
|
|
1004
|
+
$ search-hub merge session-v4 session-v9 --name combined # Merge with custom name
|
|
1005
|
+
$ search-hub merge session-a session-b session-c # Merge three sessions
|
|
1006
|
+
$ search-hub merge session-v4 session-v9 --dry-run # Preview merge`).action(
|
|
1007
|
+
async (sessionIds, options) => {
|
|
1008
|
+
const globalOpts = program.opts();
|
|
1009
|
+
try {
|
|
1010
|
+
if (sessionIds.length < 2) {
|
|
1011
|
+
if (!globalOpts.quiet) {
|
|
1012
|
+
console.error("Error: At least two session IDs are required for merge");
|
|
1013
|
+
}
|
|
1014
|
+
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const sessionsDir = await getSessionsDir(globalOpts);
|
|
1018
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1019
|
+
for (const sessionId of sessionIds) {
|
|
1020
|
+
try {
|
|
1021
|
+
const session = await loadSession(sessionId, sessionsDir);
|
|
1022
|
+
sessions.set(sessionId, session);
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
if (!globalOpts.quiet) {
|
|
1025
|
+
console.error(
|
|
1026
|
+
`Error loading session '${sessionId}': ${error instanceof Error ? error.message : "Failed to load session"}`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
process.exitCode = EXIT_CODES.SESSION_ERROR;
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const validation = validateMergeSources(sessions);
|
|
1034
|
+
if (!validation.valid) {
|
|
1035
|
+
if (!globalOpts.quiet) {
|
|
1036
|
+
console.error(`Error: ${validation.error}`);
|
|
1037
|
+
}
|
|
1038
|
+
process.exitCode = EXIT_CODES.SESSION_ERROR;
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const sessionArticles = /* @__PURE__ */ new Map();
|
|
1042
|
+
for (const [sessionId, session] of sessions) {
|
|
1043
|
+
const articles = await loadSessionArticles(session, sessionId, sessionsDir);
|
|
1044
|
+
sessionArticles.set(sessionId, articles);
|
|
1045
|
+
}
|
|
1046
|
+
const mergeResult = mergeArticles(sessionArticles);
|
|
1047
|
+
const sources = [...sessions.entries()].map(([id, session]) => ({
|
|
1048
|
+
id,
|
|
1049
|
+
name: session.name,
|
|
1050
|
+
count: mergeResult.perSession.get(id) ?? 0
|
|
1051
|
+
}));
|
|
1052
|
+
const byProviderCounts = /* @__PURE__ */ new Map();
|
|
1053
|
+
for (const [provider, articles] of mergeResult.byProvider) {
|
|
1054
|
+
byProviderCounts.set(provider, articles.length);
|
|
1055
|
+
}
|
|
1056
|
+
const firstSession = sessions.values().next().value;
|
|
1057
|
+
const mergeName = options?.name ?? (firstSession ? firstSession.name + "-merged" : "merged");
|
|
1058
|
+
if (options?.dryRun) {
|
|
1059
|
+
const outputData2 = {
|
|
1060
|
+
sessionId: "(dry-run)",
|
|
1061
|
+
totalBefore: mergeResult.totalBefore,
|
|
1062
|
+
totalAfter: mergeResult.totalAfter,
|
|
1063
|
+
duplicatesRemoved: mergeResult.duplicatesRemoved,
|
|
1064
|
+
sources,
|
|
1065
|
+
byProvider: byProviderCounts
|
|
1066
|
+
};
|
|
1067
|
+
if (options.json) {
|
|
1068
|
+
console.log(formatMergeJson(outputData2));
|
|
1069
|
+
} else if (!globalOpts.quiet) {
|
|
1070
|
+
console.log(formatMergeOutput(outputData2));
|
|
1071
|
+
}
|
|
1072
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const sessionSources = [...sessions.entries()].map(([id, session]) => ({
|
|
1076
|
+
id,
|
|
1077
|
+
name: session.name
|
|
1078
|
+
}));
|
|
1079
|
+
const mergedSession = await createMergedSession({
|
|
1080
|
+
name: mergeName,
|
|
1081
|
+
sources: sessionSources,
|
|
1082
|
+
byProvider: mergeResult.byProvider,
|
|
1083
|
+
totalRetrieved: mergeResult.totalAfter,
|
|
1084
|
+
sessionsDir,
|
|
1085
|
+
sourceSessionIds: sessionIds
|
|
1086
|
+
});
|
|
1087
|
+
const outputData = {
|
|
1088
|
+
sessionId: mergedSession.id,
|
|
1089
|
+
totalBefore: mergeResult.totalBefore,
|
|
1090
|
+
totalAfter: mergeResult.totalAfter,
|
|
1091
|
+
duplicatesRemoved: mergeResult.duplicatesRemoved,
|
|
1092
|
+
sources,
|
|
1093
|
+
byProvider: byProviderCounts
|
|
1094
|
+
};
|
|
1095
|
+
if (options?.json) {
|
|
1096
|
+
console.log(formatMergeJson(outputData));
|
|
1097
|
+
} else if (!globalOpts.quiet) {
|
|
1098
|
+
console.log(formatMergeOutput(outputData));
|
|
1099
|
+
const suggestion = getSuggestion({
|
|
1100
|
+
command: "merge",
|
|
1101
|
+
sessionId: mergedSession.id
|
|
1102
|
+
});
|
|
1103
|
+
const suggestionText = formatSuggestion(suggestion);
|
|
1104
|
+
if (suggestionText) {
|
|
1105
|
+
console.log(suggestionText);
|
|
970
1106
|
}
|
|
971
1107
|
}
|
|
972
1108
|
process.exitCode = EXIT_CODES.SUCCESS;
|
|
@@ -1343,7 +1479,7 @@ Examples:
|
|
|
1343
1479
|
};
|
|
1344
1480
|
const result = await executeReviewMerge(mergeOptions, sessionsDir);
|
|
1345
1481
|
if (!globalOpts.quiet) {
|
|
1346
|
-
console.log(formatMergeOutput(result, options.dryRun));
|
|
1482
|
+
console.log(formatMergeOutput$1(result, options.dryRun));
|
|
1347
1483
|
if (!options.dryRun) {
|
|
1348
1484
|
const statusResult = await executeReviewStatus({ sessionId: options.session }, sessionsDir);
|
|
1349
1485
|
const suggestion = formatSuggestion(getSuggestion({
|
|
@@ -1511,6 +1647,13 @@ Examples:
|
|
|
1511
1647
|
process.exitCode = EXIT_CODES.SESSION_ERROR;
|
|
1512
1648
|
return;
|
|
1513
1649
|
}
|
|
1650
|
+
if (!await sessionExists(sessionId, sessionsDir)) {
|
|
1651
|
+
if (!globalOpts.quiet) {
|
|
1652
|
+
console.error(`Error: session '${sessionId}' not found`);
|
|
1653
|
+
}
|
|
1654
|
+
process.exitCode = EXIT_CODES.SESSION_ERROR;
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1514
1657
|
const sessionDir = join(sessionsDir, sessionId);
|
|
1515
1658
|
const notes = await loadNotes(sessionDir);
|
|
1516
1659
|
if (!globalOpts.quiet) {
|