sxng-cli 1.1.7 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +2 -2
  2. package/dist/commands/graph-add.d.ts.sync-conflict-20260629-232900-2SGIW5T.map +1 -0
  3. package/dist/commands/graph-add.js +3 -3
  4. package/dist/commands/graph-add.js.map +1 -1
  5. package/dist/commands/graph-add.js.sync-conflict-20260629-232916-2SGIW5T.map +1 -0
  6. package/dist/commands/graph-add.sync-conflict-20260629-232853-2SGIW5T.js +163 -0
  7. package/dist/commands/graph-obfuscate.js +2 -2
  8. package/dist/commands/graph-obfuscate.js.map +1 -1
  9. package/dist/commands/query-graph.d.ts.map +1 -1
  10. package/dist/commands/query-graph.js +3 -3
  11. package/dist/commands/query-graph.js.map +1 -1
  12. package/dist/commands/session.d.ts.map +1 -1
  13. package/dist/commands/session.js +2 -3
  14. package/dist/commands/session.js.map +1 -1
  15. package/dist/deep/extractor.d.sync-conflict-20260629-232902-2SGIW5T.ts +37 -0
  16. package/dist/deep/extractor.d.ts.sync-conflict-20260629-232921-2SGIW5T.map +1 -0
  17. package/dist/deep/extractor.js.sync-conflict-20260629-232921-2SGIW5T.map +1 -0
  18. package/dist/deep/extractor.sync-conflict-20260629-232903-2SGIW5T.js +203 -0
  19. package/dist/deep/graph.d.sync-conflict-20260629-232917-2SGIW5T.ts +78 -0
  20. package/dist/deep/graph.d.ts.sync-conflict-20260629-232911-2SGIW5T.map +1 -0
  21. package/dist/deep/graph.js.sync-conflict-20260629-232855-2SGIW5T.map +1 -0
  22. package/dist/deep/graph.sync-conflict-20260629-232907-2SGIW5T.js +194 -0
  23. package/dist/deep/session.d.sync-conflict-20260629-232917-2SGIW5T.ts +71 -0
  24. package/dist/deep/session.d.ts +1 -1
  25. package/dist/deep/session.d.ts.map +1 -1
  26. package/dist/deep/session.d.ts.sync-conflict-20260629-232921-2SGIW5T.map +1 -0
  27. package/dist/deep/session.js +4 -5
  28. package/dist/deep/session.js.map +1 -1
  29. package/dist/deep/session.js.sync-conflict-20260629-232854-2SGIW5T.map +1 -0
  30. package/dist/runCli.d.ts.sync-conflict-20260629-232854-2SGIW5T.map +1 -0
  31. package/dist/runCli.js +4 -4
  32. package/dist/runCli.js.map +1 -1
  33. package/dist/runCli.js.sync-conflict-20260629-232857-2SGIW5T.map +1 -0
  34. package/dist/runCli.sync-conflict-20260629-232915-2SGIW5T.js +1109 -0
  35. package/dist/runCli.sync-conflict-20260629-233530-JAULAPO.d.ts +8 -0
  36. package/dist/runCli.sync-conflict-20260629-233530-JAULAPO.d.ts.map +1 -0
  37. package/dist/runCli.sync-conflict-20260629-233530-JAULAPO.js +1111 -0
  38. package/dist/runCli.sync-conflict-20260629-233530-JAULAPO.js.map +1 -0
  39. package/package.json +1 -1
@@ -0,0 +1,1111 @@
1
+ /**
2
+ * SearXNG CLI - CLI Runner (Commander-based)
3
+ */
4
+ import { Command } from 'commander';
5
+ import { readFileSync } from 'fs';
6
+ import { fileURLToPath } from 'url';
7
+ import { dirname, join } from 'path';
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
10
+ import { createSuccessEnvelope, createErrorEnvelope } from './protocol.js';
11
+ import { config } from './config.js';
12
+ import { initConfig } from './init.js';
13
+ import { runExtract } from './commands/extract.js';
14
+ import { runQueryGraph } from './commands/query-graph.js';
15
+ import { runGraphAdd } from './commands/graph-add.js';
16
+ import { runGraphObfuscateCommand } from './commands/graph-obfuscate.js';
17
+ import { ContentExtractor } from './deep/extractor.js';
18
+ import { rrf } from './deep/rrf.js';
19
+ import { normalizeUrl } from './deep/dedupe.js';
20
+ import { deserializeGraph, serializeGraph, graphStats, resultId } from './deep/graph.js';
21
+ import { initSessionDir, resolveSessionPath, appendSessionResults, updateSessionGraph, loadSessionResults, loadSessionGraph } from './deep/session.js';
22
+ import { runSessionList, runSessionDelete } from './commands/session.js';
23
+ import { graphPreprocess } from './deep/graph-preprocess.js';
24
+ import { checkQueryRedundancy } from './deep/query-redundancy.js';
25
+ import { assessResultQuality } from './deep/quality-assess.js';
26
+ import { generateQuerySuggestions } from './deep/query-suggest.js';
27
+ import { determineSearchStage, getStrategyInfo } from './deep/search-strategy.js';
28
+ import { analyzeRecoveryOptions } from './deep/recovery-analysis.js';
29
+ import { getSessionAnalysis } from './deep/iteration-data.js';
30
+ import { runGraphExplore } from './commands/graph-explore.js';
31
+ import { runGraphDrill } from './commands/graph-drill.js';
32
+ import { runGraphTraverse } from './commands/graph-traverse.js';
33
+ import { runGraphSearch } from './commands/graph-search.js';
34
+ import { DirectedGraph } from 'graphology';
35
+ import { writeFileSync, existsSync } from 'fs';
36
+ function formatAsMarkdown(data) {
37
+ const lines = [];
38
+ lines.push(`## Search: ${data.query || 'Unknown'}`);
39
+ lines.push('');
40
+ const results = data.results || [];
41
+ if (results.length > 0) {
42
+ lines.push(`**${results.length}** results`);
43
+ lines.push('');
44
+ for (let i = 0; i < results.length; i++) {
45
+ const r = results[i];
46
+ let scoreIndicator = '';
47
+ if (r.score !== undefined && r.score !== null) {
48
+ const normalizedScore = r.score > 1 ? r.score : r.score * 100;
49
+ scoreIndicator = ` [${normalizedScore.toFixed(1)}]`;
50
+ }
51
+ lines.push(`### ${i + 1}. [${r.title || 'No Title'}](${r.url || '#'})${scoreIndicator}`);
52
+ lines.push('');
53
+ if (r.content) {
54
+ lines.push(r.content);
55
+ lines.push('');
56
+ }
57
+ const meta = [];
58
+ meta.push(`Engine: ${r.engine || 'unknown'}`);
59
+ if (r.category)
60
+ meta.push(`Category: ${r.category}`);
61
+ if (r.publishedDate)
62
+ meta.push(`Published: ${r.publishedDate}`);
63
+ lines.push(meta.join(' · '));
64
+ lines.push('');
65
+ }
66
+ }
67
+ else {
68
+ lines.push('No results found.');
69
+ lines.push('');
70
+ }
71
+ if (data.unresponsiveEngines && data.unresponsiveEngines.length > 0) {
72
+ const unresponsive = data.unresponsiveEngines.map((item) => Array.isArray(item) ? item[0] : item).join(', ');
73
+ lines.push(`*Unresponsive: ${unresponsive}*`);
74
+ lines.push('');
75
+ }
76
+ if (data.suggestions && data.suggestions.length > 0) {
77
+ lines.push('**Suggestions:** ' + data.suggestions.join(' · '));
78
+ lines.push('');
79
+ }
80
+ return lines.join('\n');
81
+ }
82
+ function formatOutput(data, format) {
83
+ if (format === 'md')
84
+ return formatAsMarkdown(data);
85
+ return JSON.stringify(data, null, 2);
86
+ }
87
+ function formatPreprocessAsMarkdown(data) {
88
+ const lines = [];
89
+ lines.push('## Graph Preprocess Results');
90
+ lines.push('');
91
+ lines.push(`**Strategy:** ${data.tokenizationStrategy}`);
92
+ lines.push(`**Coverage:** ${data.resultsWithContent}/${data.totalResults} results with content`);
93
+ lines.push(`**Rounds:** ${data.roundsCovered}`);
94
+ if (data.coOccurrenceTruncated)
95
+ lines.push('*Co-occurrence matrix was truncated*');
96
+ lines.push('');
97
+ if (data.tfidfTerms?.length) {
98
+ lines.push('### Top TF-IDF Terms');
99
+ lines.push('');
100
+ lines.push('| Term | TF-IDF | Doc Freq |');
101
+ lines.push('|------|--------|----------|');
102
+ for (const t of data.tfidfTerms.slice(0, 20)) {
103
+ lines.push(`| ${t.term} | ${t.tfidf.toFixed(2)} | ${t.docFreq} |`);
104
+ }
105
+ lines.push('');
106
+ }
107
+ if (data.coOccurrences?.length) {
108
+ lines.push('### Co-occurrences');
109
+ lines.push('');
110
+ lines.push('| Term 1 | Term 2 | Count |');
111
+ lines.push('|--------|--------|-------|');
112
+ for (const p of data.coOccurrences.slice(0, 20)) {
113
+ lines.push(`| ${p.term1} | ${p.term2} | ${p.count} |`);
114
+ }
115
+ lines.push('');
116
+ }
117
+ if (data.existingEntities?.length) {
118
+ lines.push('### Existing Entities');
119
+ lines.push('');
120
+ for (const e of data.existingEntities) {
121
+ lines.push(`- ${e.label} (degree: ${e.degree}${e.entityType ? `, type: ${e.entityType}` : ''})`);
122
+ }
123
+ lines.push('');
124
+ }
125
+ return lines.join('\n');
126
+ }
127
+ function formatQualityAsMarkdown(data) {
128
+ const lines = [];
129
+ lines.push('## Result Quality Assessment');
130
+ lines.push('');
131
+ const verdictEmoji = { good: '✓', acceptable: '△', poor: '✗' };
132
+ lines.push(`**Verdict:** ${data.verdict.toUpperCase()} ${verdictEmoji[data.verdict] || ''}`);
133
+ lines.push('');
134
+ const breakdown = data.breakdown;
135
+ if (breakdown) {
136
+ lines.push('| Indicator | Value | Threshold | Pass |');
137
+ lines.push('|-----------|-------|-----------|------|');
138
+ const indicators = ['resultCount', 'contentDepth', 'entityRichness', 'sourceDiversity', 'novelty'];
139
+ const labels = {
140
+ resultCount: 'Result Count',
141
+ contentDepth: 'Content Depth',
142
+ entityRichness: 'Entity Richness',
143
+ sourceDiversity: 'Source Diversity',
144
+ novelty: 'Novelty',
145
+ };
146
+ for (const key of indicators) {
147
+ const ind = breakdown[key];
148
+ if (ind) {
149
+ const valueStr = key === 'novelty' ? ind.value.toFixed(2) : String(Math.round(ind.value * 100) / 100);
150
+ const thStr = key === 'novelty' ? ind.threshold.toFixed(2) : String(ind.threshold);
151
+ lines.push(`| ${labels[key] || key} | ${valueStr} | ${thStr} | ${ind.pass ? '✓' : '✗'} |`);
152
+ }
153
+ }
154
+ lines.push('');
155
+ }
156
+ if (data.failedIndicators?.length > 0) {
157
+ lines.push(`**Failed:** ${data.failedIndicators.join(', ')}`);
158
+ lines.push('');
159
+ }
160
+ return lines.join('\n');
161
+ }
162
+ function formatSuggestAsMarkdown(data) {
163
+ const lines = [];
164
+ lines.push('## Query Suggestion Data');
165
+ lines.push('');
166
+ lines.push(`**Current Stage:** ${data.currentStage}`);
167
+ lines.push('');
168
+ if (data.topEntities?.length) {
169
+ lines.push('### Top Entities');
170
+ lines.push('');
171
+ lines.push('| Entity | Obfuscated Label | Degree | Frequency | Type |');
172
+ lines.push('|--------|------------------|--------|-----------|------|');
173
+ for (const e of data.topEntities.slice(0, 15)) {
174
+ lines.push(`| ${e.label} | ${e.obfuscatedLabel || '-'} | ${e.degree} | ${e.frequency} | ${e.entityType || '-'} |`);
175
+ }
176
+ lines.push('');
177
+ }
178
+ if (data.unexploredDomains?.length) {
179
+ lines.push('### Unexplored Domains');
180
+ lines.push('');
181
+ for (const d of data.unexploredDomains) {
182
+ lines.push(`- ${d}`);
183
+ }
184
+ lines.push('');
185
+ }
186
+ if (data.roundHistory?.length) {
187
+ lines.push('### Round History');
188
+ lines.push('');
189
+ lines.push('| Round | Query | Results |');
190
+ lines.push('|-------|-------|---------|');
191
+ for (const r of data.roundHistory) {
192
+ lines.push(`| ${r.round} | ${r.query} | ${r.resultCount} |`);
193
+ }
194
+ lines.push('');
195
+ }
196
+ if (data.qualityLastRound) {
197
+ lines.push(`**Last Round Quality:** ${data.qualityLastRound.verdict}`);
198
+ if (data.qualityLastRound.failedIndicators?.length) {
199
+ lines.push(` (failed: ${data.qualityLastRound.failedIndicators.join(', ')})`);
200
+ }
201
+ lines.push('');
202
+ }
203
+ return lines.join('\n');
204
+ }
205
+ function formatStrategyAsMarkdown(data) {
206
+ const lines = [];
207
+ lines.push('## Search Strategy Info');
208
+ lines.push('');
209
+ lines.push(`**Current Stage:** ${data.currentStage}`);
210
+ lines.push(`**Round:** ${data.roundNumber}`);
211
+ lines.push(`**Growth Rate:** ${data.growthRate.toFixed(2)}`);
212
+ lines.push('');
213
+ lines.push(`**Recommended Engines:** ${data.recommendedEngines?.join(', ') || '-'}`);
214
+ lines.push(`**Recommended Categories:** ${data.recommendedCategories?.join(', ') || '-'}`);
215
+ if (data.transitionReason) {
216
+ lines.push('');
217
+ lines.push(`**Transition Reason:** ${data.transitionReason}`);
218
+ }
219
+ lines.push('');
220
+ return lines.join('\n');
221
+ }
222
+ function formatRecoveryAsMarkdown(data) {
223
+ const lines = [];
224
+ lines.push('## Recovery Analysis');
225
+ lines.push('');
226
+ const verdictEmoji = { good: '✓', acceptable: '△', poor: '✗' };
227
+ lines.push(`**Quality:** ${data.qualityScore.verdict.toUpperCase()} ${verdictEmoji[data.qualityScore.verdict] || ''}`);
228
+ lines.push(`**Consecutive Failures:** ${data.recentFailures}`);
229
+ lines.push('');
230
+ if (data.lastSuccessfulRound) {
231
+ lines.push(`**Last Successful Round:** Round ${data.lastSuccessfulRound.round} — "${data.lastSuccessfulRound.query}"`);
232
+ lines.push('');
233
+ }
234
+ if (data.availableStrategies.length > 0) {
235
+ lines.push('### Available Strategies');
236
+ lines.push('');
237
+ for (const s of data.availableStrategies) {
238
+ lines.push(`**${s.strategy}**`);
239
+ lines.push(`- Reason: ${s.reason}`);
240
+ lines.push(`- Suggestion: ${s.suggestion}`);
241
+ if (s.backtrackTo) {
242
+ lines.push(`- Backtrack to: Round ${s.backtrackTo.round} — "${s.backtrackTo.query}"`);
243
+ }
244
+ lines.push('');
245
+ }
246
+ }
247
+ if (data.roundQualityHistory.length > 0) {
248
+ lines.push('### Quality History');
249
+ lines.push('');
250
+ lines.push('| Round | Query | Verdict | Failed |');
251
+ lines.push('|-------|-------|---------|--------|');
252
+ for (const r of data.roundQualityHistory) {
253
+ lines.push(`| ${r.round} | ${r.query} | ${r.verdict} | ${r.failedIndicators.join(', ') || '-'} |`);
254
+ }
255
+ lines.push('');
256
+ }
257
+ return lines.join('\n');
258
+ }
259
+ function formatSessionReportAsMarkdown(data) {
260
+ const lines = [];
261
+ lines.push('## Session Report');
262
+ lines.push('');
263
+ // Quality summary
264
+ const q = data.quality;
265
+ const verdictEmoji = { good: '✓', acceptable: '△', poor: '✗' };
266
+ lines.push(`### Quality: ${q.verdict.toUpperCase()} ${verdictEmoji[q.verdict] || ''}`);
267
+ lines.push('');
268
+ if (q.breakdown) {
269
+ const indicators = ['resultCount', 'contentDepth', 'entityRichness', 'sourceDiversity', 'novelty'];
270
+ const labels = {
271
+ resultCount: 'Result Count', contentDepth: 'Content Depth',
272
+ entityRichness: 'Entity Richness', sourceDiversity: 'Source Diversity', novelty: 'Novelty',
273
+ };
274
+ for (const key of indicators) {
275
+ const ind = q.breakdown[key];
276
+ if (ind) {
277
+ const valueStr = key === 'novelty' ? ind.value.toFixed(2) : String(Math.round(ind.value * 100) / 100);
278
+ lines.push(`- ${labels[key]}: ${valueStr} (threshold: ${ind.threshold}) ${ind.pass ? '✓' : '✗'}`);
279
+ }
280
+ }
281
+ lines.push('');
282
+ }
283
+ // Strategy
284
+ const s = data.strategy;
285
+ lines.push(`### Strategy: ${s.currentStage}`);
286
+ lines.push(`- Round: ${s.roundNumber}`);
287
+ lines.push(`- Growth Rate: ${s.growthRate.toFixed(2)}`);
288
+ lines.push(`- Engines: ${s.recommendedEngines.join(', ')}`);
289
+ lines.push(`- Categories: ${s.recommendedCategories.join(', ')}`);
290
+ if (s.transitionReason)
291
+ lines.push(`- Transition: ${s.transitionReason}`);
292
+ lines.push('');
293
+ // Top entities
294
+ if (data.suggestions.topEntities?.length) {
295
+ lines.push('### Top Entities');
296
+ lines.push('');
297
+ for (const e of data.suggestions.topEntities.slice(0, 5)) {
298
+ lines.push(`- ${e.label} (degree: ${e.degree}, freq: ${e.frequency})`);
299
+ }
300
+ lines.push('');
301
+ }
302
+ // Recovery hint
303
+ if (q.verdict !== 'good') {
304
+ lines.push(`> Recovery: ${data.recovery.availableStrategies.length} strategies available. Use \`sxng recovery-analysis <session>\` for details.`);
305
+ lines.push('');
306
+ }
307
+ return lines.join('\n');
308
+ }
309
+ /** Load query strings from session graph history. */
310
+ function loadSessionQueryHistory(sessionDir) {
311
+ const graph = loadSessionGraph(sessionDir);
312
+ const queries = [];
313
+ graph.forEachNode((node, attrs) => {
314
+ if (attrs.type === 'query' && attrs.query) {
315
+ queries.push(attrs.query);
316
+ }
317
+ });
318
+ return queries;
319
+ }
320
+ function addToGraph(graphFile, results) {
321
+ let graph;
322
+ if (existsSync(graphFile)) {
323
+ try {
324
+ const raw = readFileSync(graphFile, 'utf-8');
325
+ const parsed = JSON.parse(raw);
326
+ const graphData = parsed.status === 'ok' && parsed.data?.graph
327
+ ? parsed.data.graph
328
+ : (parsed.nodes && parsed.edges ? parsed : null);
329
+ if (graphData) {
330
+ graph = deserializeGraph(graphData);
331
+ }
332
+ else {
333
+ graph = new DirectedGraph();
334
+ }
335
+ }
336
+ catch {
337
+ graph = new DirectedGraph();
338
+ }
339
+ }
340
+ else {
341
+ graph = new DirectedGraph();
342
+ }
343
+ for (let i = 0; i < results.length; i++) {
344
+ const r = results[i];
345
+ const id = resultId(r.url);
346
+ if (!graph.hasNode(id)) {
347
+ graph.mergeNode(id, {
348
+ type: 'result',
349
+ label: r.title,
350
+ url: r.url,
351
+ title: r.title,
352
+ rank: i + 1,
353
+ });
354
+ }
355
+ }
356
+ const serialized = serializeGraph(graph);
357
+ const stats = graphStats(graph);
358
+ writeFileSync(graphFile, JSON.stringify({ status: 'ok', data: { graph: serialized, stats } }, null, 2), 'utf-8');
359
+ }
360
+ async function runSearch(service, options) {
361
+ const queries = options.queries || (options.query ? [options.query] : []);
362
+ if (queries.length === 0) {
363
+ const envelope = createErrorEnvelope('MISSING_QUERY', 'No search query provided', { hint: 'Use: sxng "your search query" or sxng --queries "q1,q2,q3"' });
364
+ console.log(JSON.stringify(envelope, null, 2));
365
+ return 1;
366
+ }
367
+ if (options.engines && config.allowedEngines.length > 0) {
368
+ const invalid = options.engines.filter(e => !config.allowedEngines.includes(e));
369
+ if (invalid.length > 0) {
370
+ const envelope = createErrorEnvelope('INVALID_ENGINES', `Engines not allowed: ${invalid.join(', ')}`, { hint: `Allowed engines: ${config.allowedEngines.join(', ')}` });
371
+ console.log(JSON.stringify(envelope, null, 2));
372
+ return 1;
373
+ }
374
+ }
375
+ const limit = options.limit ?? config.defaultLimit;
376
+ if (options.session) {
377
+ const resolved = resolveSessionPath(options.session);
378
+ options.session = resolved;
379
+ initSessionDir(resolved, options.owner, options.desc, options.query);
380
+ }
381
+ // Pre-search redundancy check
382
+ if (options.redundancy && options.session) {
383
+ const history = loadSessionQueryHistory(options.session);
384
+ const redundancyConfig = {
385
+ jaccardThreshold: 0.7,
386
+ action: options.redundancy,
387
+ };
388
+ const adjustedQueries = [];
389
+ const skippedQueries = [];
390
+ for (const q of queries) {
391
+ const result = checkQueryRedundancy(q, history, redundancyConfig);
392
+ if (result.isRedundant) {
393
+ if (redundancyConfig.action === 'skip') {
394
+ skippedQueries.push(q);
395
+ continue;
396
+ }
397
+ else if (redundancyConfig.action === 'adjust' && result.adjustedQuery) {
398
+ adjustedQueries.push(result.adjustedQuery);
399
+ }
400
+ else {
401
+ // warn — proceed but output warning
402
+ adjustedQueries.push(q);
403
+ }
404
+ // Output redundancy info
405
+ if (redundancyConfig.action === 'warn' || redundancyConfig.action === 'adjust') {
406
+ const envelope = createSuccessEnvelope({
407
+ redundancyWarning: {
408
+ query: q,
409
+ adjustedQuery: result.adjustedQuery,
410
+ maxSimilarity: result.maxSimilarity,
411
+ similarQueries: result.similarQueries,
412
+ action: redundancyConfig.action,
413
+ },
414
+ });
415
+ console.error(JSON.stringify(envelope, null, 2));
416
+ }
417
+ }
418
+ else {
419
+ adjustedQueries.push(q);
420
+ }
421
+ }
422
+ if (skippedQueries.length > 0 && skippedQueries.length === queries.length) {
423
+ // All queries skipped
424
+ const envelope = createSuccessEnvelope({
425
+ message: 'All queries skipped due to redundancy',
426
+ skippedQueries,
427
+ });
428
+ console.log(JSON.stringify(envelope, null, 2));
429
+ return 0;
430
+ }
431
+ // Replace queries with adjusted ones (skipped ones removed)
432
+ queries.length = 0;
433
+ queries.push(...adjustedQueries);
434
+ }
435
+ try {
436
+ if (queries.length === 1 && !options.merge) {
437
+ const searchOptions = {
438
+ query: queries[0],
439
+ engines: options.engines,
440
+ categories: options.categories,
441
+ limit,
442
+ page: options.page,
443
+ language: options.language,
444
+ timeRange: options.timeRange,
445
+ };
446
+ const results = await service.search(searchOptions);
447
+ let sessionInfo = null;
448
+ if (options.session) {
449
+ sessionInfo = appendSessionResults(options.session, results.results);
450
+ for (const query of queries) {
451
+ updateSessionGraph(options.session, query, results.results.map(r => ({
452
+ url: r.url,
453
+ title: r.title,
454
+ })));
455
+ }
456
+ }
457
+ const outputFormat = (options.format || config.defaultFormat);
458
+ let displayResults = results.results;
459
+ if (options.session) {
460
+ const allSessionResults = loadSessionResults(options.session);
461
+ const rankings = [
462
+ results.results.map(r => ({ id: normalizeUrl(r.url), ...r })),
463
+ allSessionResults.map(r => ({ id: normalizeUrl(r.url), ...r })),
464
+ ];
465
+ const fused = rrf(rankings);
466
+ const urlMap = new Map();
467
+ for (const r of results.results)
468
+ urlMap.set(normalizeUrl(r.url), r);
469
+ for (const r of allSessionResults)
470
+ urlMap.set(normalizeUrl(r.url), r);
471
+ displayResults = fused
472
+ .map(item => {
473
+ const original = urlMap.get(item.id);
474
+ return original ? { ...original, score: item.score } : null;
475
+ })
476
+ .filter(Boolean);
477
+ if (limit > 0)
478
+ displayResults = displayResults.slice(0, limit);
479
+ }
480
+ const displayData = {
481
+ query: results.query,
482
+ totalResults: results.numberOfResults,
483
+ results: displayResults,
484
+ answers: results.answers,
485
+ suggestions: results.suggestions,
486
+ unresponsiveEngines: results.unresponsiveEngines,
487
+ ...(sessionInfo ? { session: { dir: options.session, added: sessionInfo.added, total: sessionInfo.total } } : {}),
488
+ };
489
+ if (outputFormat === 'md') {
490
+ console.log(formatOutput(displayData, 'md'));
491
+ }
492
+ else {
493
+ console.log(JSON.stringify(createSuccessEnvelope({
494
+ ...displayData,
495
+ returnedResults: displayResults.length,
496
+ }), null, 2));
497
+ }
498
+ if (options.graph) {
499
+ addToGraph(options.graph, results.results);
500
+ }
501
+ return 0;
502
+ }
503
+ const allResponses = [];
504
+ for (const query of queries) {
505
+ const searchOptions = {
506
+ query,
507
+ engines: options.engines,
508
+ categories: options.categories,
509
+ limit,
510
+ page: options.page,
511
+ language: options.language,
512
+ timeRange: options.timeRange,
513
+ };
514
+ const response = await service.search(searchOptions);
515
+ allResponses.push(response);
516
+ }
517
+ let rankings = allResponses.map(resp => resp.results.map((r) => ({ id: normalizeUrl(r.url), ...r })));
518
+ let mergeData = null;
519
+ if (options.merge) {
520
+ try {
521
+ const raw = readFileSync(options.merge, 'utf-8');
522
+ mergeData = JSON.parse(raw);
523
+ let mergeResults = mergeData;
524
+ if (mergeData.status === 'ok' && mergeData.data) {
525
+ mergeResults = mergeData.data;
526
+ }
527
+ const historicalResults = (mergeResults.results || []);
528
+ if (historicalResults.length > 0) {
529
+ rankings.push(historicalResults.map((r) => ({ id: normalizeUrl(r.url), ...r })));
530
+ }
531
+ }
532
+ catch (error) {
533
+ const envelope = createErrorEnvelope('MERGE_FILE_FAILED', `Failed to read merge file: ${options.merge}`, { hint: 'Ensure the file exists and contains valid search results JSON' });
534
+ console.log(JSON.stringify(envelope, null, 2));
535
+ return 1;
536
+ }
537
+ }
538
+ const rrfFused = rrf(rankings);
539
+ const allResults = [];
540
+ const allSuggestions = [];
541
+ const allAnswers = [];
542
+ const allUnresponsive = [];
543
+ for (const resp of allResponses) {
544
+ allResults.push(...resp.results);
545
+ for (const s of resp.suggestions) {
546
+ if (!allSuggestions.includes(s))
547
+ allSuggestions.push(s);
548
+ }
549
+ for (const a of resp.answers) {
550
+ if (!allAnswers.includes(a))
551
+ allAnswers.push(a);
552
+ }
553
+ for (const u of resp.unresponsiveEngines) {
554
+ const name = Array.isArray(u) ? u[0] : u;
555
+ if (!allUnresponsive.includes(name))
556
+ allUnresponsive.push(name);
557
+ }
558
+ }
559
+ if (mergeData) {
560
+ let mergeResults = mergeData;
561
+ if (mergeData.status === 'ok' && mergeData.data) {
562
+ mergeResults = mergeData.data;
563
+ }
564
+ const historical = mergeResults.results || [];
565
+ allResults.push(...historical);
566
+ if (mergeResults.suggestions) {
567
+ for (const s of mergeResults.suggestions) {
568
+ if (!allSuggestions.includes(s))
569
+ allSuggestions.push(s);
570
+ }
571
+ }
572
+ if (mergeResults.answers) {
573
+ for (const a of mergeResults.answers) {
574
+ if (!allAnswers.includes(a))
575
+ allAnswers.push(a);
576
+ }
577
+ }
578
+ }
579
+ const urlMap = new Map();
580
+ for (const r of allResults) {
581
+ const norm = normalizeUrl(r.url);
582
+ if (!urlMap.has(norm))
583
+ urlMap.set(norm, r);
584
+ }
585
+ let fusedResults = rrfFused
586
+ .map((item) => {
587
+ const original = urlMap.get(item.id);
588
+ if (!original)
589
+ return null;
590
+ return { ...original, score: item.score };
591
+ })
592
+ .filter(Boolean);
593
+ if (limit > 0) {
594
+ fusedResults = fusedResults.slice(0, limit);
595
+ }
596
+ let sessionInfo = null;
597
+ if (options.session) {
598
+ sessionInfo = appendSessionResults(options.session, fusedResults);
599
+ for (const query of queries) {
600
+ updateSessionGraph(options.session, query, fusedResults.map(r => ({
601
+ url: r.url,
602
+ title: r.title,
603
+ })));
604
+ }
605
+ }
606
+ const displayQuery = queries.length > 1 ? queries.join(' · ') : queries[0];
607
+ const outputFormat = (options.format || config.defaultFormat);
608
+ const displayData = {
609
+ query: displayQuery,
610
+ queries,
611
+ totalResults: allResults.length,
612
+ results: fusedResults,
613
+ answers: allAnswers,
614
+ suggestions: allSuggestions,
615
+ unresponsiveEngines: allUnresponsive,
616
+ ...(sessionInfo ? { session: { dir: options.session, added: sessionInfo.added, total: sessionInfo.total } } : {}),
617
+ };
618
+ if (outputFormat === 'md') {
619
+ console.log(formatOutput(displayData, 'md'));
620
+ }
621
+ else if (outputFormat === 'json') {
622
+ console.log(JSON.stringify(createSuccessEnvelope({
623
+ ...displayData,
624
+ returnedResults: fusedResults.length,
625
+ }), null, 2));
626
+ }
627
+ else {
628
+ console.log(formatOutput(displayData, outputFormat));
629
+ }
630
+ if (options.graph) {
631
+ addToGraph(options.graph, fusedResults);
632
+ }
633
+ return 0;
634
+ }
635
+ catch (error) {
636
+ const envelope = createErrorEnvelope('SEARCH_FAILED', error instanceof Error ? error.message : 'Search request failed', {
637
+ retryable: true,
638
+ hint: 'Check your network connection and SearXNG server status'
639
+ });
640
+ console.log(JSON.stringify(envelope, null, 2));
641
+ return 1;
642
+ }
643
+ }
644
+ export function createProgram() {
645
+ const program = new Command();
646
+ program
647
+ .name('sxng')
648
+ .description('SearXNG CLI - Web Search Tool')
649
+ .version(pkg.version)
650
+ .argument('[query]', 'Search query')
651
+ .option('-e, --engines <engines>', 'Comma-separated list of search engines')
652
+ .option('-c, --categories <cats>', 'Comma-separated list of categories')
653
+ .option('-l, --limit <n>', 'Maximum number of results', val => parseInt(val, 10))
654
+ .option('-p, --page <n>', 'Page number for pagination', val => parseInt(val, 10))
655
+ .option('--lang <code>', 'Language code (e.g., en, zh, ja)')
656
+ .option('--time <range>', 'Time range: day, week, month, year, all')
657
+ .option('-f, --format <fmt>', 'Output format: md (default), json')
658
+ .option('--queries <q1,q2,q3>', 'Multi-query with RRF fusion')
659
+ .option('--merge <file>', 'Merge new results with previous search JSON')
660
+ .option('--search-session <dir|new>', 'Session dir, or "new" to auto-create')
661
+ .option('--owner <name>', 'Session owner (stored in meta.json)')
662
+ .option('--desc <text>', 'Session description (stored in meta.json)')
663
+ .option('--graph <file>', 'Save search result metadata to knowledge graph file')
664
+ .option('--redundancy <action>', 'Query redundancy check: warn | adjust | skip')
665
+ .option('--quality', 'Assess result quality for current session')
666
+ .option('--threshold-override <json>', 'Override quality thresholds (JSON, e.g. \'{"resultCount":10}\')')
667
+ .option('--health', 'Check SearXNG server health')
668
+ .option('--engines-list', 'List available search engines')
669
+ .option('--categories-list', 'List available categories')
670
+ .allowUnknownOption(false);
671
+ program
672
+ .command('init')
673
+ .description('Interactive configuration setup')
674
+ .action(async () => {
675
+ const code = await initConfig();
676
+ process.exit(code);
677
+ });
678
+ program
679
+ .command('extract')
680
+ .description('Extract article content from URLs or session results')
681
+ .option('--urls <url1,url2>', 'URLs to extract content from')
682
+ .option('--from-json <file>', 'Extract from search results JSON file')
683
+ .option('--session <dir>', 'Extract from session results and merge content back')
684
+ .option('--obscura', 'Use Obscura as fallback for JS-heavy pages')
685
+ .option('--obscura-path <path>', 'Path to Obscura binary')
686
+ .option('--obscura-dump <format>', 'Obscura dump format: html, markdown', 'html')
687
+ .option('--jina', 'Use Jina Reader (r.jina.ai) as fallback')
688
+ .action(async (opts) => {
689
+ const extractor = new ContentExtractor({
690
+ obscura: opts.obscura ?? false,
691
+ obscuraPath: opts.obscuraPath,
692
+ obscuraDumpFormat: opts.obscuraDump === 'markdown' ? 'markdown' : 'html',
693
+ jina: opts.jina ?? false,
694
+ });
695
+ const extractOptions = {
696
+ urls: opts.urls?.split(',').map((u) => u.trim()).filter(Boolean),
697
+ fromJson: opts.fromJson,
698
+ session: opts.session,
699
+ };
700
+ const code = await runExtract(extractor, extractOptions);
701
+ process.exit(code);
702
+ });
703
+ program
704
+ .command('query-graph')
705
+ .argument('<path>', 'Graph file or session name')
706
+ .description('[DEPRECATED] Query subgraph via BFS. Use graph-explore + graph-drill instead.')
707
+ .option('--seeds <s1,s2>', 'Seed nodes for BFS')
708
+ .option('--depth <n>', 'BFS depth (default: 2)', val => parseInt(val, 10), 2)
709
+ .option('--strategy <strategy>', 'Sampling strategy: augmented_chain | dual_core_bridge | community_core_path | deep_chain | mixed')
710
+ .option('-f, --format <fmt>', 'Output format: md, json')
711
+ .action(async (path, opts) => {
712
+ console.warn('[DEPRECATED] query-graph is deprecated. Use graph-explore + graph-drill instead.');
713
+ const graphFormat = opts.format === 'json' ? 'json' : 'md';
714
+ const code = await runQueryGraph({
715
+ graphFile: path,
716
+ seeds: opts.seeds?.split(',').map((s) => s.trim()).filter(Boolean) || [],
717
+ depth: opts.depth ?? 2,
718
+ format: graphFormat,
719
+ strategy: opts.strategy,
720
+ });
721
+ process.exit(code);
722
+ });
723
+ program
724
+ .command('graph-add')
725
+ .argument('<path>', 'Graph file or session name')
726
+ .description('Add entities/edges to knowledge graph')
727
+ .requiredOption('--data <json>', 'JSON with entities/edges')
728
+ .action(async (path, opts) => {
729
+ const code = await runGraphAdd({
730
+ graphFile: path,
731
+ data: opts.data,
732
+ });
733
+ process.exit(code);
734
+ });
735
+ program
736
+ .command('session-list')
737
+ .description('List all sessions')
738
+ .action(async () => {
739
+ const code = await runSessionList();
740
+ process.exit(code);
741
+ });
742
+ program
743
+ .command('session-delete')
744
+ .argument('[names]', 'Comma-separated session names to delete')
745
+ .option('--older <h>', 'Delete sessions older than N hours', val => parseFloat(val))
746
+ .action(async (names, opts) => {
747
+ const nameList = (names || '').split(',').map((n) => n.trim()).filter(Boolean);
748
+ const code = await runSessionDelete(nameList, opts.older);
749
+ process.exit(code);
750
+ });
751
+ program
752
+ .command('graph-obfuscate')
753
+ .argument('<path>', 'Graph file or session name')
754
+ .description('List obfuscation candidates or apply fallback rules (experimental)')
755
+ .option('--list', 'List entities needing obfuscation (default)')
756
+ .option('--fallback-rules', 'Apply simple rule-based obfuscation (experimental)')
757
+ .option('--skip-types <t1,t2>', 'Entity types to skip')
758
+ .option('-f, --format <fmt>', 'Output format: json (default), md')
759
+ .addHelpText('after', `
760
+ Examples:
761
+ sxng graph-obfuscate my-session --list
762
+ sxng graph-obfuscate my-session --fallback-rules --format md
763
+
764
+ Note: --fallback-rules is experimental and may produce low-quality obfuscation labels.
765
+ Recommended workflow: use --list to get candidates, then have an LLM generate obfuscated labels,
766
+ and write them back via graph-add.`)
767
+ .action(async (path, opts) => {
768
+ const code = await runGraphObfuscateCommand({
769
+ graphFile: path,
770
+ list: opts.list ?? !opts.fallbackRules,
771
+ fallbackRules: opts.fallbackRules ?? false,
772
+ format: opts.format === 'md' ? 'md' : 'json',
773
+ skipEntityTypes: opts.skipTypes?.split(',').map((t) => t.trim()).filter(Boolean),
774
+ });
775
+ process.exit(code);
776
+ });
777
+ program
778
+ .command('graph-preprocess')
779
+ .argument('<session>', 'Session directory or name')
780
+ .description('Preprocess session data: TF-IDF, co-occurrence, entity context')
781
+ .option('--top <n>', 'Top N terms to return', val => parseInt(val, 10), 30)
782
+ .option('--co-occurrence-threshold <n>', 'Min co-occurrence count', val => parseInt(val, 10), 2)
783
+ .option('--max-terms <n>', 'Max terms for co-occurrence matrix', val => parseInt(val, 10), 50)
784
+ .option('-f, --format <fmt>', 'Output format: json (default), md')
785
+ .addHelpText('after', `
786
+ Examples:
787
+ sxng graph-preprocess my-session
788
+ sxng graph-preprocess my-session --top 50 --format md`)
789
+ .action(async (session, opts) => {
790
+ const sessionDir = resolveSessionPath(session);
791
+ const result = graphPreprocess(sessionDir, {
792
+ top: opts.top,
793
+ coOccurrenceThreshold: opts.coOccurrenceThreshold,
794
+ maxTermsForCoOccurrence: opts.maxTerms,
795
+ });
796
+ if (opts.format === 'md') {
797
+ console.log(formatPreprocessAsMarkdown(result));
798
+ }
799
+ else {
800
+ console.log(JSON.stringify(createSuccessEnvelope(result), null, 2));
801
+ }
802
+ process.exit(0);
803
+ });
804
+ program
805
+ .command('suggest-queries')
806
+ .argument('<session>', 'Session directory or name')
807
+ .description('Output query suggestion data for Agent follow-up query generation')
808
+ .option('-f, --format <fmt>', 'Output format: json (default), md')
809
+ .addHelpText('after', `
810
+ Examples:
811
+ sxng suggest-queries my-session
812
+ sxng suggest-queries my-session --format md`)
813
+ .action(async (session, opts) => {
814
+ const sessionDir = resolveSessionPath(session);
815
+ const graph = loadSessionGraph(sessionDir);
816
+ const results = loadSessionResults(sessionDir);
817
+ const stage = determineSearchStage(graph);
818
+ const quality = assessResultQuality(results, results, graph);
819
+ const data = generateQuerySuggestions(graph, results, stage, quality);
820
+ if (opts.format === 'md') {
821
+ console.log(formatSuggestAsMarkdown(data));
822
+ }
823
+ else {
824
+ console.log(JSON.stringify(createSuccessEnvelope(data), null, 2));
825
+ }
826
+ process.exit(0);
827
+ });
828
+ program
829
+ .command('strategy-info')
830
+ .argument('<session>', 'Session directory or name')
831
+ .description('Output current search stage recommendation (broad vs targeted)')
832
+ .option('--broad-rounds <n>', 'Rounds before transition check', val => parseInt(val, 10), 2)
833
+ .option('--transition-threshold <n>', 'Growth rate threshold for stage transition', parseFloat, 0.2)
834
+ .option('-f, --format <fmt>', 'Output format: json (default), md')
835
+ .addHelpText('after', `
836
+ Examples:
837
+ sxng strategy-info my-session
838
+ sxng strategy-info my-session --format md`)
839
+ .action(async (session, opts) => {
840
+ const sessionDir = resolveSessionPath(session);
841
+ const graph = loadSessionGraph(sessionDir);
842
+ const info = getStrategyInfo(graph, {
843
+ broadRounds: opts.broadRounds,
844
+ transitionThreshold: opts.transitionThreshold,
845
+ });
846
+ if (opts.format === 'md') {
847
+ console.log(formatStrategyAsMarkdown(info));
848
+ }
849
+ else {
850
+ console.log(JSON.stringify(createSuccessEnvelope(info), null, 2));
851
+ }
852
+ process.exit(0);
853
+ });
854
+ program
855
+ .command('recovery-analysis')
856
+ .argument('<session>', 'Session directory or name')
857
+ .description('Output recovery strategy analysis for Agent decision-making')
858
+ .option('-f, --format <fmt>', 'Output format: json (default), md')
859
+ .addHelpText('after', `
860
+ Examples:
861
+ sxng recovery-analysis my-session
862
+ sxng recovery-analysis my-session --format md`)
863
+ .action(async (session, opts) => {
864
+ const sessionDir = resolveSessionPath(session);
865
+ const graph = loadSessionGraph(sessionDir);
866
+ const results = loadSessionResults(sessionDir);
867
+ const quality = assessResultQuality(results, results, graph);
868
+ const analysis = analyzeRecoveryOptions(graph, results, quality);
869
+ if (opts.format === 'md') {
870
+ console.log(formatRecoveryAsMarkdown(analysis));
871
+ }
872
+ else {
873
+ console.log(JSON.stringify(createSuccessEnvelope(analysis), null, 2));
874
+ }
875
+ process.exit(0);
876
+ });
877
+ program
878
+ .command('session-report')
879
+ .argument('<session>', 'Session directory or name')
880
+ .description('Show session quality history and stage progression')
881
+ .option('-f, --format <fmt>', 'Output format: json (default), md')
882
+ .addHelpText('after', `
883
+ Examples:
884
+ sxng session-report my-session
885
+ sxng session-report my-session --format md`)
886
+ .action(async (session, opts) => {
887
+ const sessionDir = resolveSessionPath(session);
888
+ const graph = loadSessionGraph(sessionDir);
889
+ const results = loadSessionResults(sessionDir);
890
+ const analysis = getSessionAnalysis(graph, results);
891
+ if (opts.format === 'md') {
892
+ console.log(formatSessionReportAsMarkdown(analysis));
893
+ }
894
+ else {
895
+ console.log(JSON.stringify(createSuccessEnvelope(analysis), null, 2));
896
+ }
897
+ process.exit(0);
898
+ });
899
+ program
900
+ .command('graph-explore')
901
+ .argument('<session>', 'Session directory or name')
902
+ .description('List all relations around a seed entity (Agent graph navigation)')
903
+ .requiredOption('--seed <entity>', 'Seed entity label to explore')
904
+ .option('-f, --format <fmt>', 'Output format: md (default), json')
905
+ .addHelpText('after', `
906
+ Examples:
907
+ sxng graph-explore my-session --seed "tokio"
908
+ sxng graph-explore my-session --seed "tokio" --format json
909
+
910
+ See also: graph-drill, graph-search`)
911
+ .action(async (session, opts) => {
912
+ const code = await runGraphExplore({
913
+ session,
914
+ seed: opts.seed,
915
+ format: opts.format === 'json' ? 'json' : 'md',
916
+ });
917
+ process.exit(code);
918
+ });
919
+ program
920
+ .command('graph-drill')
921
+ .argument('<session>', 'Session directory or name')
922
+ .description('Follow specific relations from an entity (Agent graph navigation)')
923
+ .requiredOption('--seed <entity>', 'Seed entity label')
924
+ .requiredOption('--relations <list>', 'Comma-separated relation types to follow')
925
+ .option('-f, --format <fmt>', 'Output format: md (default), json')
926
+ .addHelpText('after', `
927
+ Examples:
928
+ sxng graph-drill my-session --seed "tokio" --relations "alternative_to"
929
+ sxng graph-drill my-session --seed "tokio" --relations "alternative_to,depends_on" --format json
930
+
931
+ See also: graph-explore`)
932
+ .action(async (session, opts) => {
933
+ const code = await runGraphDrill({
934
+ session,
935
+ seed: opts.seed,
936
+ relations: opts.relations.split(',').map((r) => r.trim()).filter(Boolean),
937
+ format: opts.format === 'json' ? 'json' : 'md',
938
+ });
939
+ process.exit(code);
940
+ });
941
+ program
942
+ .command('graph-traverse')
943
+ .argument('<session>', 'Session directory or name')
944
+ .description('Traverse a reasoning path by path node ID (requires path nodes from graph-preprocess)')
945
+ .requiredOption('--path <path-id>', 'Path node ID (e.g. p:chain_001)')
946
+ .option('-f, --format <fmt>', 'Output format: md (default), json')
947
+ .addHelpText('after', `
948
+ Examples:
949
+ sxng graph-traverse my-session --path "p:chain_001"
950
+ sxng graph-traverse my-session --path "p:chain_001" --format json
951
+
952
+ Note: Path nodes are created by graph-preprocess. If no paths exist,
953
+ the command will list available path node IDs.`)
954
+ .action(async (session, opts) => {
955
+ const code = await runGraphTraverse({
956
+ session,
957
+ pathId: opts.path,
958
+ format: opts.format === 'json' ? 'json' : 'md',
959
+ });
960
+ process.exit(code);
961
+ });
962
+ program
963
+ .command('graph-search')
964
+ .argument('<session>', 'Session directory or name')
965
+ .description('Keyword search across entity labels (discover entities before exploring)')
966
+ .requiredOption('--keyword <term>', 'Search keyword')
967
+ .option('-l, --limit <n>', 'Max results', val => parseInt(val, 10), 10)
968
+ .option('-f, --format <fmt>', 'Output format: md (default), json')
969
+ .addHelpText('after', `
970
+ Examples:
971
+ sxng graph-search my-session --keyword "async"
972
+ sxng graph-search my-session --keyword "tokio" --limit 5 --format json
973
+
974
+ See also: graph-explore (for viewing relations of a known entity)`)
975
+ .action(async (session, opts) => {
976
+ const code = await runGraphSearch({
977
+ session,
978
+ keyword: opts.keyword,
979
+ limit: opts.limit,
980
+ format: opts.format === 'json' ? 'json' : 'md',
981
+ });
982
+ process.exit(code);
983
+ });
984
+ return program;
985
+ }
986
+ export async function runCli(args, service) {
987
+ const program = createProgram();
988
+ // Custom action for default command (search)
989
+ // Commander passes the [query] argument as first param, then opts
990
+ program.action(async (query, opts) => {
991
+ const queryString = query || '';
992
+ if (opts.version) {
993
+ console.log(program.version());
994
+ process.exit(0);
995
+ return;
996
+ }
997
+ if (opts.health) {
998
+ const health = await service.healthCheck();
999
+ const envelope = health.status === 'healthy'
1000
+ ? createSuccessEnvelope(health)
1001
+ : createErrorEnvelope('HEALTH_CHECK_FAILED', health.error || 'SearXNG server is not responding', { hint: `Check if SearXNG is running at ${config.baseUrl}` });
1002
+ console.log(JSON.stringify(envelope, null, 2));
1003
+ process.exit(health.status === 'healthy' ? 0 : 1);
1004
+ return;
1005
+ }
1006
+ if (opts.quality) {
1007
+ if (!opts.searchSession) {
1008
+ const envelope = createErrorEnvelope('QUALITY_NO_SESSION', '--quality requires --session', { hint: 'Use: sxng --session <name> --quality' });
1009
+ console.log(JSON.stringify(envelope, null, 2));
1010
+ process.exit(1);
1011
+ return;
1012
+ }
1013
+ const sessionDir = resolveSessionPath(opts.searchSession);
1014
+ const sessionResults = loadSessionResults(sessionDir);
1015
+ const sessionGraph = loadSessionGraph(sessionDir);
1016
+ let thresholdOverride;
1017
+ if (opts.thresholdOverride) {
1018
+ try {
1019
+ thresholdOverride = JSON.parse(opts.thresholdOverride);
1020
+ }
1021
+ catch {
1022
+ const envelope = createErrorEnvelope('INVALID_THRESHOLD_OVERRIDE', '--threshold-override must be valid JSON', { hint: 'Example: \'{"resultCount":10}\'' });
1023
+ console.log(JSON.stringify(envelope, null, 2));
1024
+ process.exit(1);
1025
+ return;
1026
+ }
1027
+ }
1028
+ const quality = assessResultQuality(sessionResults, sessionResults, sessionGraph, thresholdOverride);
1029
+ const outputFormat = (opts.format || config.defaultFormat);
1030
+ if (outputFormat === 'md') {
1031
+ console.log(formatQualityAsMarkdown(quality));
1032
+ }
1033
+ else {
1034
+ console.log(JSON.stringify(createSuccessEnvelope(quality), null, 2));
1035
+ }
1036
+ process.exit(0);
1037
+ return;
1038
+ }
1039
+ if (opts.enginesList) {
1040
+ try {
1041
+ const engines = await service.getEngines();
1042
+ if (engines.length === 0) {
1043
+ const envelope = createErrorEnvelope('ENGINES_FETCH_EMPTY', 'No engines returned from SearXNG server', { hint: 'Check if SearXNG server is properly configured' });
1044
+ console.log(JSON.stringify(envelope, null, 2));
1045
+ process.exit(1);
1046
+ return;
1047
+ }
1048
+ const envelope = createSuccessEnvelope({ engines, source: 'server' });
1049
+ console.log(JSON.stringify(envelope, null, 2));
1050
+ process.exit(0);
1051
+ }
1052
+ catch (error) {
1053
+ const envelope = createErrorEnvelope('ENGINES_FETCH_FAILED', error instanceof Error ? error.message : 'Failed to fetch engines', { hint: 'Check your network connection and SearXNG server status' });
1054
+ console.log(JSON.stringify(envelope, null, 2));
1055
+ process.exit(1);
1056
+ }
1057
+ return;
1058
+ }
1059
+ if (opts.categoriesList) {
1060
+ try {
1061
+ const categories = await service.getCategories();
1062
+ if (categories.length === 0) {
1063
+ const envelope = createErrorEnvelope('CATEGORIES_FETCH_EMPTY', 'No categories returned from SearXNG server', { hint: 'Check if SearXNG server is properly configured' });
1064
+ console.log(JSON.stringify(envelope, null, 2));
1065
+ process.exit(1);
1066
+ return;
1067
+ }
1068
+ const envelope = createSuccessEnvelope({ categories, source: 'server' });
1069
+ console.log(JSON.stringify(envelope, null, 2));
1070
+ process.exit(0);
1071
+ }
1072
+ catch (error) {
1073
+ const envelope = createErrorEnvelope('CATEGORIES_FETCH_FAILED', error instanceof Error ? error.message : 'Failed to fetch categories', { hint: 'Check your network connection and SearXNG server status' });
1074
+ console.log(JSON.stringify(envelope, null, 2));
1075
+ process.exit(1);
1076
+ }
1077
+ return;
1078
+ }
1079
+ const queries = opts.queries?.split(',').map((q) => q.trim()).filter(Boolean);
1080
+ const code = await runSearch(service, {
1081
+ query: queryString,
1082
+ queries,
1083
+ engines: opts.engines?.split(',').map((e) => e.trim()).filter(Boolean),
1084
+ categories: opts.categories?.split(',').map((c) => c.trim()).filter(Boolean),
1085
+ limit: opts.limit,
1086
+ page: opts.page,
1087
+ language: opts.lang,
1088
+ timeRange: opts.time,
1089
+ format: opts.format,
1090
+ session: opts.searchSession,
1091
+ owner: opts.owner,
1092
+ desc: opts.desc,
1093
+ graph: opts.graph,
1094
+ merge: opts.merge,
1095
+ redundancy: opts.redundancy,
1096
+ });
1097
+ process.exit(code);
1098
+ });
1099
+ try {
1100
+ await program.parseAsync(args, { from: 'user' });
1101
+ }
1102
+ catch (error) {
1103
+ if (error instanceof Error && error.name === 'CommanderError') {
1104
+ console.error(error.message);
1105
+ return 1;
1106
+ }
1107
+ throw error;
1108
+ }
1109
+ return null;
1110
+ }
1111
+ //# sourceMappingURL=runCli.sync-conflict-20260629-233530-JAULAPO.js.map