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.
- package/README.md +2 -2
- package/dist/commands/graph-add.d.ts.sync-conflict-20260629-232900-2SGIW5T.map +1 -0
- package/dist/commands/graph-add.js +3 -3
- package/dist/commands/graph-add.js.map +1 -1
- package/dist/commands/graph-add.js.sync-conflict-20260629-232916-2SGIW5T.map +1 -0
- package/dist/commands/graph-add.sync-conflict-20260629-232853-2SGIW5T.js +163 -0
- package/dist/commands/graph-obfuscate.js +2 -2
- package/dist/commands/graph-obfuscate.js.map +1 -1
- package/dist/commands/query-graph.d.ts.map +1 -1
- package/dist/commands/query-graph.js +3 -3
- package/dist/commands/query-graph.js.map +1 -1
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +2 -3
- package/dist/commands/session.js.map +1 -1
- package/dist/deep/extractor.d.sync-conflict-20260629-232902-2SGIW5T.ts +37 -0
- package/dist/deep/extractor.d.ts.sync-conflict-20260629-232921-2SGIW5T.map +1 -0
- package/dist/deep/extractor.js.sync-conflict-20260629-232921-2SGIW5T.map +1 -0
- package/dist/deep/extractor.sync-conflict-20260629-232903-2SGIW5T.js +203 -0
- package/dist/deep/graph.d.sync-conflict-20260629-232917-2SGIW5T.ts +78 -0
- package/dist/deep/graph.d.ts.sync-conflict-20260629-232911-2SGIW5T.map +1 -0
- package/dist/deep/graph.js.sync-conflict-20260629-232855-2SGIW5T.map +1 -0
- package/dist/deep/graph.sync-conflict-20260629-232907-2SGIW5T.js +194 -0
- package/dist/deep/session.d.sync-conflict-20260629-232917-2SGIW5T.ts +71 -0
- package/dist/deep/session.d.ts +1 -1
- package/dist/deep/session.d.ts.map +1 -1
- package/dist/deep/session.d.ts.sync-conflict-20260629-232921-2SGIW5T.map +1 -0
- package/dist/deep/session.js +4 -5
- package/dist/deep/session.js.map +1 -1
- package/dist/deep/session.js.sync-conflict-20260629-232854-2SGIW5T.map +1 -0
- package/dist/runCli.d.ts.sync-conflict-20260629-232854-2SGIW5T.map +1 -0
- package/dist/runCli.js +4 -4
- package/dist/runCli.js.map +1 -1
- package/dist/runCli.js.sync-conflict-20260629-232857-2SGIW5T.map +1 -0
- package/dist/runCli.sync-conflict-20260629-232915-2SGIW5T.js +1109 -0
- package/dist/runCli.sync-conflict-20260629-233530-JAULAPO.d.ts +8 -0
- package/dist/runCli.sync-conflict-20260629-233530-JAULAPO.d.ts.map +1 -0
- package/dist/runCli.sync-conflict-20260629-233530-JAULAPO.js +1111 -0
- package/dist/runCli.sync-conflict-20260629-233530-JAULAPO.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1109 @@
|
|
|
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
|
+
.action(async (opts) => {
|
|
688
|
+
const extractor = new ContentExtractor({
|
|
689
|
+
obscura: opts.obscura ?? false,
|
|
690
|
+
obscuraPath: opts.obscuraPath,
|
|
691
|
+
obscuraDumpFormat: opts.obscuraDump === 'markdown' ? 'markdown' : 'html',
|
|
692
|
+
});
|
|
693
|
+
const extractOptions = {
|
|
694
|
+
urls: opts.urls?.split(',').map((u) => u.trim()).filter(Boolean),
|
|
695
|
+
fromJson: opts.fromJson,
|
|
696
|
+
session: opts.session,
|
|
697
|
+
};
|
|
698
|
+
const code = await runExtract(extractor, extractOptions);
|
|
699
|
+
process.exit(code);
|
|
700
|
+
});
|
|
701
|
+
program
|
|
702
|
+
.command('query-graph')
|
|
703
|
+
.argument('<path>', 'Graph file or session name')
|
|
704
|
+
.description('[DEPRECATED] Query subgraph via BFS. Use graph-explore + graph-drill instead.')
|
|
705
|
+
.option('--seeds <s1,s2>', 'Seed nodes for BFS')
|
|
706
|
+
.option('--depth <n>', 'BFS depth (default: 2)', val => parseInt(val, 10), 2)
|
|
707
|
+
.option('--strategy <strategy>', 'Sampling strategy: augmented_chain | dual_core_bridge | community_core_path | deep_chain | mixed')
|
|
708
|
+
.option('-f, --format <fmt>', 'Output format: md, json')
|
|
709
|
+
.action(async (path, opts) => {
|
|
710
|
+
console.warn('[DEPRECATED] query-graph is deprecated. Use graph-explore + graph-drill instead.');
|
|
711
|
+
const graphFormat = opts.format === 'json' ? 'json' : 'md';
|
|
712
|
+
const code = await runQueryGraph({
|
|
713
|
+
graphFile: path,
|
|
714
|
+
seeds: opts.seeds?.split(',').map((s) => s.trim()).filter(Boolean) || [],
|
|
715
|
+
depth: opts.depth ?? 2,
|
|
716
|
+
format: graphFormat,
|
|
717
|
+
strategy: opts.strategy,
|
|
718
|
+
});
|
|
719
|
+
process.exit(code);
|
|
720
|
+
});
|
|
721
|
+
program
|
|
722
|
+
.command('graph-add')
|
|
723
|
+
.argument('<path>', 'Graph file or session name')
|
|
724
|
+
.description('Add entities/edges to knowledge graph')
|
|
725
|
+
.requiredOption('--data <json>', 'JSON with entities/edges')
|
|
726
|
+
.action(async (path, opts) => {
|
|
727
|
+
const code = await runGraphAdd({
|
|
728
|
+
graphFile: path,
|
|
729
|
+
data: opts.data,
|
|
730
|
+
});
|
|
731
|
+
process.exit(code);
|
|
732
|
+
});
|
|
733
|
+
program
|
|
734
|
+
.command('session-list')
|
|
735
|
+
.description('List all sessions')
|
|
736
|
+
.action(async () => {
|
|
737
|
+
const code = await runSessionList();
|
|
738
|
+
process.exit(code);
|
|
739
|
+
});
|
|
740
|
+
program
|
|
741
|
+
.command('session-delete')
|
|
742
|
+
.argument('[names]', 'Comma-separated session names to delete')
|
|
743
|
+
.option('--older <h>', 'Delete sessions older than N hours', val => parseFloat(val))
|
|
744
|
+
.action(async (names, opts) => {
|
|
745
|
+
const nameList = (names || '').split(',').map((n) => n.trim()).filter(Boolean);
|
|
746
|
+
const code = await runSessionDelete(nameList, opts.older);
|
|
747
|
+
process.exit(code);
|
|
748
|
+
});
|
|
749
|
+
program
|
|
750
|
+
.command('graph-obfuscate')
|
|
751
|
+
.argument('<path>', 'Graph file or session name')
|
|
752
|
+
.description('List obfuscation candidates or apply fallback rules (experimental)')
|
|
753
|
+
.option('--list', 'List entities needing obfuscation (default)')
|
|
754
|
+
.option('--fallback-rules', 'Apply simple rule-based obfuscation (experimental)')
|
|
755
|
+
.option('--skip-types <t1,t2>', 'Entity types to skip')
|
|
756
|
+
.option('-f, --format <fmt>', 'Output format: json (default), md')
|
|
757
|
+
.addHelpText('after', `
|
|
758
|
+
Examples:
|
|
759
|
+
sxng graph-obfuscate my-session --list
|
|
760
|
+
sxng graph-obfuscate my-session --fallback-rules --format md
|
|
761
|
+
|
|
762
|
+
Note: --fallback-rules is experimental and may produce low-quality obfuscation labels.
|
|
763
|
+
Recommended workflow: use --list to get candidates, then have an LLM generate obfuscated labels,
|
|
764
|
+
and write them back via graph-add.`)
|
|
765
|
+
.action(async (path, opts) => {
|
|
766
|
+
const code = await runGraphObfuscateCommand({
|
|
767
|
+
graphFile: path,
|
|
768
|
+
list: opts.list ?? !opts.fallbackRules,
|
|
769
|
+
fallbackRules: opts.fallbackRules ?? false,
|
|
770
|
+
format: opts.format === 'md' ? 'md' : 'json',
|
|
771
|
+
skipEntityTypes: opts.skipTypes?.split(',').map((t) => t.trim()).filter(Boolean),
|
|
772
|
+
});
|
|
773
|
+
process.exit(code);
|
|
774
|
+
});
|
|
775
|
+
program
|
|
776
|
+
.command('graph-preprocess')
|
|
777
|
+
.argument('<session>', 'Session directory or name')
|
|
778
|
+
.description('Preprocess session data: TF-IDF, co-occurrence, entity context')
|
|
779
|
+
.option('--top <n>', 'Top N terms to return', val => parseInt(val, 10), 30)
|
|
780
|
+
.option('--co-occurrence-threshold <n>', 'Min co-occurrence count', val => parseInt(val, 10), 2)
|
|
781
|
+
.option('--max-terms <n>', 'Max terms for co-occurrence matrix', val => parseInt(val, 10), 50)
|
|
782
|
+
.option('-f, --format <fmt>', 'Output format: json (default), md')
|
|
783
|
+
.addHelpText('after', `
|
|
784
|
+
Examples:
|
|
785
|
+
sxng graph-preprocess my-session
|
|
786
|
+
sxng graph-preprocess my-session --top 50 --format md`)
|
|
787
|
+
.action(async (session, opts) => {
|
|
788
|
+
const sessionDir = resolveSessionPath(session);
|
|
789
|
+
const result = graphPreprocess(sessionDir, {
|
|
790
|
+
top: opts.top,
|
|
791
|
+
coOccurrenceThreshold: opts.coOccurrenceThreshold,
|
|
792
|
+
maxTermsForCoOccurrence: opts.maxTerms,
|
|
793
|
+
});
|
|
794
|
+
if (opts.format === 'md') {
|
|
795
|
+
console.log(formatPreprocessAsMarkdown(result));
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
console.log(JSON.stringify(createSuccessEnvelope(result), null, 2));
|
|
799
|
+
}
|
|
800
|
+
process.exit(0);
|
|
801
|
+
});
|
|
802
|
+
program
|
|
803
|
+
.command('suggest-queries')
|
|
804
|
+
.argument('<session>', 'Session directory or name')
|
|
805
|
+
.description('Output query suggestion data for Agent follow-up query generation')
|
|
806
|
+
.option('-f, --format <fmt>', 'Output format: json (default), md')
|
|
807
|
+
.addHelpText('after', `
|
|
808
|
+
Examples:
|
|
809
|
+
sxng suggest-queries my-session
|
|
810
|
+
sxng suggest-queries my-session --format md`)
|
|
811
|
+
.action(async (session, opts) => {
|
|
812
|
+
const sessionDir = resolveSessionPath(session);
|
|
813
|
+
const graph = loadSessionGraph(sessionDir);
|
|
814
|
+
const results = loadSessionResults(sessionDir);
|
|
815
|
+
const stage = determineSearchStage(graph);
|
|
816
|
+
const quality = assessResultQuality(results, results, graph);
|
|
817
|
+
const data = generateQuerySuggestions(graph, results, stage, quality);
|
|
818
|
+
if (opts.format === 'md') {
|
|
819
|
+
console.log(formatSuggestAsMarkdown(data));
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
console.log(JSON.stringify(createSuccessEnvelope(data), null, 2));
|
|
823
|
+
}
|
|
824
|
+
process.exit(0);
|
|
825
|
+
});
|
|
826
|
+
program
|
|
827
|
+
.command('strategy-info')
|
|
828
|
+
.argument('<session>', 'Session directory or name')
|
|
829
|
+
.description('Output current search stage recommendation (broad vs targeted)')
|
|
830
|
+
.option('--broad-rounds <n>', 'Rounds before transition check', val => parseInt(val, 10), 2)
|
|
831
|
+
.option('--transition-threshold <n>', 'Growth rate threshold for stage transition', parseFloat, 0.2)
|
|
832
|
+
.option('-f, --format <fmt>', 'Output format: json (default), md')
|
|
833
|
+
.addHelpText('after', `
|
|
834
|
+
Examples:
|
|
835
|
+
sxng strategy-info my-session
|
|
836
|
+
sxng strategy-info my-session --format md`)
|
|
837
|
+
.action(async (session, opts) => {
|
|
838
|
+
const sessionDir = resolveSessionPath(session);
|
|
839
|
+
const graph = loadSessionGraph(sessionDir);
|
|
840
|
+
const info = getStrategyInfo(graph, {
|
|
841
|
+
broadRounds: opts.broadRounds,
|
|
842
|
+
transitionThreshold: opts.transitionThreshold,
|
|
843
|
+
});
|
|
844
|
+
if (opts.format === 'md') {
|
|
845
|
+
console.log(formatStrategyAsMarkdown(info));
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
console.log(JSON.stringify(createSuccessEnvelope(info), null, 2));
|
|
849
|
+
}
|
|
850
|
+
process.exit(0);
|
|
851
|
+
});
|
|
852
|
+
program
|
|
853
|
+
.command('recovery-analysis')
|
|
854
|
+
.argument('<session>', 'Session directory or name')
|
|
855
|
+
.description('Output recovery strategy analysis for Agent decision-making')
|
|
856
|
+
.option('-f, --format <fmt>', 'Output format: json (default), md')
|
|
857
|
+
.addHelpText('after', `
|
|
858
|
+
Examples:
|
|
859
|
+
sxng recovery-analysis my-session
|
|
860
|
+
sxng recovery-analysis my-session --format md`)
|
|
861
|
+
.action(async (session, opts) => {
|
|
862
|
+
const sessionDir = resolveSessionPath(session);
|
|
863
|
+
const graph = loadSessionGraph(sessionDir);
|
|
864
|
+
const results = loadSessionResults(sessionDir);
|
|
865
|
+
const quality = assessResultQuality(results, results, graph);
|
|
866
|
+
const analysis = analyzeRecoveryOptions(graph, results, quality);
|
|
867
|
+
if (opts.format === 'md') {
|
|
868
|
+
console.log(formatRecoveryAsMarkdown(analysis));
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
console.log(JSON.stringify(createSuccessEnvelope(analysis), null, 2));
|
|
872
|
+
}
|
|
873
|
+
process.exit(0);
|
|
874
|
+
});
|
|
875
|
+
program
|
|
876
|
+
.command('session-report')
|
|
877
|
+
.argument('<session>', 'Session directory or name')
|
|
878
|
+
.description('Show session quality history and stage progression')
|
|
879
|
+
.option('-f, --format <fmt>', 'Output format: json (default), md')
|
|
880
|
+
.addHelpText('after', `
|
|
881
|
+
Examples:
|
|
882
|
+
sxng session-report my-session
|
|
883
|
+
sxng session-report my-session --format md`)
|
|
884
|
+
.action(async (session, opts) => {
|
|
885
|
+
const sessionDir = resolveSessionPath(session);
|
|
886
|
+
const graph = loadSessionGraph(sessionDir);
|
|
887
|
+
const results = loadSessionResults(sessionDir);
|
|
888
|
+
const analysis = getSessionAnalysis(graph, results);
|
|
889
|
+
if (opts.format === 'md') {
|
|
890
|
+
console.log(formatSessionReportAsMarkdown(analysis));
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
console.log(JSON.stringify(createSuccessEnvelope(analysis), null, 2));
|
|
894
|
+
}
|
|
895
|
+
process.exit(0);
|
|
896
|
+
});
|
|
897
|
+
program
|
|
898
|
+
.command('graph-explore')
|
|
899
|
+
.argument('<session>', 'Session directory or name')
|
|
900
|
+
.description('List all relations around a seed entity (Agent graph navigation)')
|
|
901
|
+
.requiredOption('--seed <entity>', 'Seed entity label to explore')
|
|
902
|
+
.option('-f, --format <fmt>', 'Output format: md (default), json')
|
|
903
|
+
.addHelpText('after', `
|
|
904
|
+
Examples:
|
|
905
|
+
sxng graph-explore my-session --seed "tokio"
|
|
906
|
+
sxng graph-explore my-session --seed "tokio" --format json
|
|
907
|
+
|
|
908
|
+
See also: graph-drill, graph-search`)
|
|
909
|
+
.action(async (session, opts) => {
|
|
910
|
+
const code = await runGraphExplore({
|
|
911
|
+
session,
|
|
912
|
+
seed: opts.seed,
|
|
913
|
+
format: opts.format === 'json' ? 'json' : 'md',
|
|
914
|
+
});
|
|
915
|
+
process.exit(code);
|
|
916
|
+
});
|
|
917
|
+
program
|
|
918
|
+
.command('graph-drill')
|
|
919
|
+
.argument('<session>', 'Session directory or name')
|
|
920
|
+
.description('Follow specific relations from an entity (Agent graph navigation)')
|
|
921
|
+
.requiredOption('--seed <entity>', 'Seed entity label')
|
|
922
|
+
.requiredOption('--relations <list>', 'Comma-separated relation types to follow')
|
|
923
|
+
.option('-f, --format <fmt>', 'Output format: md (default), json')
|
|
924
|
+
.addHelpText('after', `
|
|
925
|
+
Examples:
|
|
926
|
+
sxng graph-drill my-session --seed "tokio" --relations "alternative_to"
|
|
927
|
+
sxng graph-drill my-session --seed "tokio" --relations "alternative_to,depends_on" --format json
|
|
928
|
+
|
|
929
|
+
See also: graph-explore`)
|
|
930
|
+
.action(async (session, opts) => {
|
|
931
|
+
const code = await runGraphDrill({
|
|
932
|
+
session,
|
|
933
|
+
seed: opts.seed,
|
|
934
|
+
relations: opts.relations.split(',').map((r) => r.trim()).filter(Boolean),
|
|
935
|
+
format: opts.format === 'json' ? 'json' : 'md',
|
|
936
|
+
});
|
|
937
|
+
process.exit(code);
|
|
938
|
+
});
|
|
939
|
+
program
|
|
940
|
+
.command('graph-traverse')
|
|
941
|
+
.argument('<session>', 'Session directory or name')
|
|
942
|
+
.description('Traverse a reasoning path by path node ID (requires path nodes from graph-preprocess)')
|
|
943
|
+
.requiredOption('--path <path-id>', 'Path node ID (e.g. p:chain_001)')
|
|
944
|
+
.option('-f, --format <fmt>', 'Output format: md (default), json')
|
|
945
|
+
.addHelpText('after', `
|
|
946
|
+
Examples:
|
|
947
|
+
sxng graph-traverse my-session --path "p:chain_001"
|
|
948
|
+
sxng graph-traverse my-session --path "p:chain_001" --format json
|
|
949
|
+
|
|
950
|
+
Note: Path nodes are created by graph-preprocess. If no paths exist,
|
|
951
|
+
the command will list available path node IDs.`)
|
|
952
|
+
.action(async (session, opts) => {
|
|
953
|
+
const code = await runGraphTraverse({
|
|
954
|
+
session,
|
|
955
|
+
pathId: opts.path,
|
|
956
|
+
format: opts.format === 'json' ? 'json' : 'md',
|
|
957
|
+
});
|
|
958
|
+
process.exit(code);
|
|
959
|
+
});
|
|
960
|
+
program
|
|
961
|
+
.command('graph-search')
|
|
962
|
+
.argument('<session>', 'Session directory or name')
|
|
963
|
+
.description('Keyword search across entity labels (discover entities before exploring)')
|
|
964
|
+
.requiredOption('--keyword <term>', 'Search keyword')
|
|
965
|
+
.option('-l, --limit <n>', 'Max results', val => parseInt(val, 10), 10)
|
|
966
|
+
.option('-f, --format <fmt>', 'Output format: md (default), json')
|
|
967
|
+
.addHelpText('after', `
|
|
968
|
+
Examples:
|
|
969
|
+
sxng graph-search my-session --keyword "async"
|
|
970
|
+
sxng graph-search my-session --keyword "tokio" --limit 5 --format json
|
|
971
|
+
|
|
972
|
+
See also: graph-explore (for viewing relations of a known entity)`)
|
|
973
|
+
.action(async (session, opts) => {
|
|
974
|
+
const code = await runGraphSearch({
|
|
975
|
+
session,
|
|
976
|
+
keyword: opts.keyword,
|
|
977
|
+
limit: opts.limit,
|
|
978
|
+
format: opts.format === 'json' ? 'json' : 'md',
|
|
979
|
+
});
|
|
980
|
+
process.exit(code);
|
|
981
|
+
});
|
|
982
|
+
return program;
|
|
983
|
+
}
|
|
984
|
+
export async function runCli(args, service) {
|
|
985
|
+
const program = createProgram();
|
|
986
|
+
// Custom action for default command (search)
|
|
987
|
+
// Commander passes the [query] argument as first param, then opts
|
|
988
|
+
program.action(async (query, opts) => {
|
|
989
|
+
const queryString = query || '';
|
|
990
|
+
if (opts.version) {
|
|
991
|
+
console.log(program.version());
|
|
992
|
+
process.exit(0);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (opts.health) {
|
|
996
|
+
const health = await service.healthCheck();
|
|
997
|
+
const envelope = health.status === 'healthy'
|
|
998
|
+
? createSuccessEnvelope(health)
|
|
999
|
+
: createErrorEnvelope('HEALTH_CHECK_FAILED', health.error || 'SearXNG server is not responding', { hint: `Check if SearXNG is running at ${config.baseUrl}` });
|
|
1000
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
1001
|
+
process.exit(health.status === 'healthy' ? 0 : 1);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (opts.quality) {
|
|
1005
|
+
if (!opts.searchSession) {
|
|
1006
|
+
const envelope = createErrorEnvelope('QUALITY_NO_SESSION', '--quality requires --session', { hint: 'Use: sxng --session <name> --quality' });
|
|
1007
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const sessionDir = resolveSessionPath(opts.searchSession);
|
|
1012
|
+
const sessionResults = loadSessionResults(sessionDir);
|
|
1013
|
+
const sessionGraph = loadSessionGraph(sessionDir);
|
|
1014
|
+
let thresholdOverride;
|
|
1015
|
+
if (opts.thresholdOverride) {
|
|
1016
|
+
try {
|
|
1017
|
+
thresholdOverride = JSON.parse(opts.thresholdOverride);
|
|
1018
|
+
}
|
|
1019
|
+
catch {
|
|
1020
|
+
const envelope = createErrorEnvelope('INVALID_THRESHOLD_OVERRIDE', '--threshold-override must be valid JSON', { hint: 'Example: \'{"resultCount":10}\'' });
|
|
1021
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
1022
|
+
process.exit(1);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const quality = assessResultQuality(sessionResults, sessionResults, sessionGraph, thresholdOverride);
|
|
1027
|
+
const outputFormat = (opts.format || config.defaultFormat);
|
|
1028
|
+
if (outputFormat === 'md') {
|
|
1029
|
+
console.log(formatQualityAsMarkdown(quality));
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
console.log(JSON.stringify(createSuccessEnvelope(quality), null, 2));
|
|
1033
|
+
}
|
|
1034
|
+
process.exit(0);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (opts.enginesList) {
|
|
1038
|
+
try {
|
|
1039
|
+
const engines = await service.getEngines();
|
|
1040
|
+
if (engines.length === 0) {
|
|
1041
|
+
const envelope = createErrorEnvelope('ENGINES_FETCH_EMPTY', 'No engines returned from SearXNG server', { hint: 'Check if SearXNG server is properly configured' });
|
|
1042
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
1043
|
+
process.exit(1);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const envelope = createSuccessEnvelope({ engines, source: 'server' });
|
|
1047
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
1048
|
+
process.exit(0);
|
|
1049
|
+
}
|
|
1050
|
+
catch (error) {
|
|
1051
|
+
const envelope = createErrorEnvelope('ENGINES_FETCH_FAILED', error instanceof Error ? error.message : 'Failed to fetch engines', { hint: 'Check your network connection and SearXNG server status' });
|
|
1052
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
1053
|
+
process.exit(1);
|
|
1054
|
+
}
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
if (opts.categoriesList) {
|
|
1058
|
+
try {
|
|
1059
|
+
const categories = await service.getCategories();
|
|
1060
|
+
if (categories.length === 0) {
|
|
1061
|
+
const envelope = createErrorEnvelope('CATEGORIES_FETCH_EMPTY', 'No categories returned from SearXNG server', { hint: 'Check if SearXNG server is properly configured' });
|
|
1062
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const envelope = createSuccessEnvelope({ categories, source: 'server' });
|
|
1067
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
1068
|
+
process.exit(0);
|
|
1069
|
+
}
|
|
1070
|
+
catch (error) {
|
|
1071
|
+
const envelope = createErrorEnvelope('CATEGORIES_FETCH_FAILED', error instanceof Error ? error.message : 'Failed to fetch categories', { hint: 'Check your network connection and SearXNG server status' });
|
|
1072
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
1073
|
+
process.exit(1);
|
|
1074
|
+
}
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const queries = opts.queries?.split(',').map((q) => q.trim()).filter(Boolean);
|
|
1078
|
+
const code = await runSearch(service, {
|
|
1079
|
+
query: queryString,
|
|
1080
|
+
queries,
|
|
1081
|
+
engines: opts.engines?.split(',').map((e) => e.trim()).filter(Boolean),
|
|
1082
|
+
categories: opts.categories?.split(',').map((c) => c.trim()).filter(Boolean),
|
|
1083
|
+
limit: opts.limit,
|
|
1084
|
+
page: opts.page,
|
|
1085
|
+
language: opts.lang,
|
|
1086
|
+
timeRange: opts.time,
|
|
1087
|
+
format: opts.format,
|
|
1088
|
+
session: opts.searchSession,
|
|
1089
|
+
owner: opts.owner,
|
|
1090
|
+
desc: opts.desc,
|
|
1091
|
+
graph: opts.graph,
|
|
1092
|
+
merge: opts.merge,
|
|
1093
|
+
redundancy: opts.redundancy,
|
|
1094
|
+
});
|
|
1095
|
+
process.exit(code);
|
|
1096
|
+
});
|
|
1097
|
+
try {
|
|
1098
|
+
await program.parseAsync(args, { from: 'user' });
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
if (error instanceof Error && error.name === 'CommanderError') {
|
|
1102
|
+
console.error(error.message);
|
|
1103
|
+
return 1;
|
|
1104
|
+
}
|
|
1105
|
+
throw error;
|
|
1106
|
+
}
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
//# sourceMappingURL=runCli.js.map
|