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