@allansantos-dev/smart-tool 0.9.0 → 0.9.2
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/CHANGELOG.md +50 -23
- package/NOTICE +32 -32
- package/README.md +117 -116
- package/atomic_io.py +94 -94
- package/bin/smart-tool.mjs +5 -1
- package/client_hooks.py +192 -192
- package/code_graph_java.mjs +49 -49
- package/code_graph_js.cjs +135 -135
- package/duplicates.py +313 -313
- package/embedding_cache.py +99 -99
- package/endpoint_sync.py +179 -179
- package/gateway.py +220 -220
- package/hook_decision.py +96 -96
- package/index_inventory.py +75 -75
- package/indexer.py +878 -878
- package/install.py +113 -11
- package/install_runtime.py +101 -35
- package/integration_metrics.py +82 -82
- package/local_embedder.py +172 -172
- package/model_defaults.py +70 -70
- package/package.json +35 -35
- package/project_insights_ui.py +38 -38
- package/project_monitor.py +204 -204
- package/project_store.py +220 -220
- package/projects_ui.py +146 -146
- package/requirements.txt +8 -8
- package/research_cache.py +171 -171
- package/research_metrics.py +110 -110
- package/router.py +582 -582
- package/secret_store.py +121 -121
- package/setup_ui.py +1011 -986
- package/smart_tool_daemon.py +3604 -3604
- package/version.py +1 -1
- package/web_adapters/browser/fetch_camoufox.py +69 -0
- package/web_adapters/node/ddg-search.mjs +89 -89
- package/web_adapters/node/open-websearch-adapter.mjs +77 -77
- package/web_adapters/node/package.json +12 -12
- package/web_adapters/node/wikipedia-search.mjs +44 -44
- package/web_search_adapters.py +193 -193
- package/web_search_external.py +288 -288
- package/web_search_health.py +88 -88
package/version.py
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""`camoufox fetch` for the installer. camoufox 0.5.6 prints a failed download and still exits 0, so the error is
|
|
2
|
+
caught here and turned into a failing exit, and the browser executable must exist afterwards. Its downloads have no
|
|
3
|
+
timeout (requests passes None, which also overrides socket.setdefaulttimeout), so every request gets one here: a
|
|
4
|
+
stalled download fails after 60 s instead of hanging. Progress is printed as plain lines (the rich bar stays
|
|
5
|
+
invisible in a pipe)."""
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import camoufox.__main__ as camoufox_cli
|
|
10
|
+
import requests
|
|
11
|
+
from camoufox import pkgman
|
|
12
|
+
|
|
13
|
+
TIMEOUT_S = (30, 60)
|
|
14
|
+
REPORT_EVERY_MB = 50
|
|
15
|
+
_webdl = pkgman.webdl
|
|
16
|
+
_update = camoufox_cli.CamoufoxUpdate.update
|
|
17
|
+
_get = requests.get
|
|
18
|
+
failures = []
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _reporting_webdl(url, desc=None, buffer=None, bar=True, progress_callback=None):
|
|
22
|
+
reported = {"mb": -REPORT_EVERY_MB}
|
|
23
|
+
|
|
24
|
+
def report(done, total):
|
|
25
|
+
mb = done // 1048576
|
|
26
|
+
if mb - reported["mb"] >= REPORT_EVERY_MB or done == total:
|
|
27
|
+
reported["mb"] = mb
|
|
28
|
+
print(f"downloaded {mb} of {total // 1048576} MB", flush=True)
|
|
29
|
+
|
|
30
|
+
return _webdl(url, desc=desc, buffer=buffer, bar=False, progress_callback=progress_callback or report)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_with_timeout(*args, **kwargs):
|
|
34
|
+
kwargs.setdefault("timeout", TIMEOUT_S)
|
|
35
|
+
return _get(*args, **kwargs)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _recording_update(self, *args, **kwargs):
|
|
39
|
+
try:
|
|
40
|
+
return _update(self, *args, **kwargs)
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
failures.append(exc)
|
|
43
|
+
raise
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def installed_executable():
|
|
47
|
+
"""The browser executable, without the download that launch_path() starts when it is missing."""
|
|
48
|
+
executable = Path(pkgman.camoufox_path(download_if_missing=False)) / pkgman.LAUNCH_FILE[pkgman.OS_NAME]
|
|
49
|
+
if not executable.is_file():
|
|
50
|
+
raise FileNotFoundError(executable)
|
|
51
|
+
return executable
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
requests.get = _get_with_timeout
|
|
56
|
+
pkgman.webdl = _reporting_webdl
|
|
57
|
+
camoufox_cli.CamoufoxUpdate.update = _recording_update
|
|
58
|
+
camoufox_cli.cli.main(["fetch"], standalone_mode=False)
|
|
59
|
+
if failures:
|
|
60
|
+
sys.exit(f"Camoufox download failed: {failures[-1]}")
|
|
61
|
+
try:
|
|
62
|
+
executable = installed_executable()
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
sys.exit(f"Camoufox is not installed after the download: {exc}")
|
|
65
|
+
print(f"Camoufox {pkgman.installed_verstr()} ready: {executable}", flush=True)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Uso: node ddg-search.mjs "consulta" [--num 8] [--region br-pt]
|
|
3
|
-
// Saída: JSON no stdout — [{ title, url, snippet }, ...]
|
|
4
|
-
import { parseArgs, stripTags } from './adapter-common.mjs';
|
|
5
|
-
|
|
6
|
-
function realUrlFromDdgHref(href) {
|
|
7
|
-
const match = /uddg=([^&]+)/.exec(href);
|
|
8
|
-
if (!match) return href.startsWith('//') ? `https:${href}` : href;
|
|
9
|
-
try {
|
|
10
|
-
return decodeURIComponent(match[1]);
|
|
11
|
-
} catch {
|
|
12
|
-
return match[1];
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async function main() {
|
|
17
|
-
const { query, num, region } = parseArgs(process.argv.slice(2),
|
|
18
|
-
'Usage: node ddg-search.mjs "query" [--num 8] [--region br-pt]', { num: 8, region: null });
|
|
19
|
-
|
|
20
|
-
const params = new URLSearchParams({ q: query });
|
|
21
|
-
if (region) params.set('kl', region);
|
|
22
|
-
const url = `https://html.duckduckgo.com/html/?${params.toString()}`;
|
|
23
|
-
|
|
24
|
-
const res = await fetch(url, {
|
|
25
|
-
headers: {
|
|
26
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36',
|
|
27
|
-
'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8',
|
|
28
|
-
},
|
|
29
|
-
signal: AbortSignal.timeout(15000),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
if (!res.ok) {
|
|
33
|
-
throw new Error(`DuckDuckGo answered HTTP ${res.status}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const html = await res.text();
|
|
37
|
-
|
|
38
|
-
// Um resultado patrocinado usa a mesma classe `result__a` no link de título que um
|
|
39
|
-
// resultado orgânico — só o container (`<div class="result ... result--ad ...">`)
|
|
40
|
-
// distingue os dois. Por isso a extração é por bloco (do início de um `<div
|
|
41
|
-
// class="result ` até o início do próximo), não por duas regexes globais pareadas por
|
|
42
|
-
// posição: título+snippet do mesmo bloco nunca desalinha com o resultado seguinte, e um
|
|
43
|
-
// bloco com `result--ad` na própria classe é descartado antes de extrair.
|
|
44
|
-
const BLOCK_START = '<div class="result ';
|
|
45
|
-
const startPositions = [];
|
|
46
|
-
for (let i = html.indexOf(BLOCK_START); i !== -1; i = html.indexOf(BLOCK_START, i + BLOCK_START.length)) {
|
|
47
|
-
startPositions.push(i);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const titleRe = /<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/;
|
|
51
|
-
const snippetRe = /<a[^>]*class="result__snippet"[^>]*href="[^"]+"[^>]*>([\s\S]*?)<\/a>/;
|
|
52
|
-
|
|
53
|
-
const results = [];
|
|
54
|
-
for (let i = 0; i < startPositions.length && results.length < num; i++) {
|
|
55
|
-
const start = startPositions[i];
|
|
56
|
-
const end = i + 1 < startPositions.length ? startPositions[i + 1] : html.length;
|
|
57
|
-
const block = html.slice(start, end);
|
|
58
|
-
|
|
59
|
-
const classEnd = block.indexOf('"', BLOCK_START.length);
|
|
60
|
-
const classAttr = block.slice(BLOCK_START.length, classEnd);
|
|
61
|
-
if (classAttr.split(/\s+/).includes('result--ad')) continue;
|
|
62
|
-
|
|
63
|
-
const titleMatch = titleRe.exec(block);
|
|
64
|
-
if (!titleMatch) continue;
|
|
65
|
-
const snippetMatch = snippetRe.exec(block);
|
|
66
|
-
|
|
67
|
-
results.push({
|
|
68
|
-
title: stripTags(titleMatch[2]),
|
|
69
|
-
url: realUrlFromDdgHref(titleMatch[1]),
|
|
70
|
-
snippet: snippetMatch ? stripTags(snippetMatch[1]) : '',
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (results.length === 0) {
|
|
75
|
-
throw new Error('No result extracted: DuckDuckGo may have blocked or changed the HTML. Try again or rephrase the query.');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
process.stdout.write(JSON.stringify(results, null, 2));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Nunca chamar process.exit() aqui: com stdout/stderr em pipe (caso do
|
|
82
|
-
// subprocess.run que consome este script), abortar o processo enquanto ainda
|
|
83
|
-
// há handles assíncronos pendentes (ex.: o timer do AbortSignal.timeout do
|
|
84
|
-
// fetch) pode disparar um crash nativo do libuv no Windows. Só marcar o
|
|
85
|
-
// exit code e deixar o event loop drenar sozinho.
|
|
86
|
-
main().catch((err) => {
|
|
87
|
-
console.error(String(err?.message ?? err));
|
|
88
|
-
process.exitCode = 1;
|
|
89
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Uso: node ddg-search.mjs "consulta" [--num 8] [--region br-pt]
|
|
3
|
+
// Saída: JSON no stdout — [{ title, url, snippet }, ...]
|
|
4
|
+
import { parseArgs, stripTags } from './adapter-common.mjs';
|
|
5
|
+
|
|
6
|
+
function realUrlFromDdgHref(href) {
|
|
7
|
+
const match = /uddg=([^&]+)/.exec(href);
|
|
8
|
+
if (!match) return href.startsWith('//') ? `https:${href}` : href;
|
|
9
|
+
try {
|
|
10
|
+
return decodeURIComponent(match[1]);
|
|
11
|
+
} catch {
|
|
12
|
+
return match[1];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const { query, num, region } = parseArgs(process.argv.slice(2),
|
|
18
|
+
'Usage: node ddg-search.mjs "query" [--num 8] [--region br-pt]', { num: 8, region: null });
|
|
19
|
+
|
|
20
|
+
const params = new URLSearchParams({ q: query });
|
|
21
|
+
if (region) params.set('kl', region);
|
|
22
|
+
const url = `https://html.duckduckgo.com/html/?${params.toString()}`;
|
|
23
|
+
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
headers: {
|
|
26
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36',
|
|
27
|
+
'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8',
|
|
28
|
+
},
|
|
29
|
+
signal: AbortSignal.timeout(15000),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
throw new Error(`DuckDuckGo answered HTTP ${res.status}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const html = await res.text();
|
|
37
|
+
|
|
38
|
+
// Um resultado patrocinado usa a mesma classe `result__a` no link de título que um
|
|
39
|
+
// resultado orgânico — só o container (`<div class="result ... result--ad ...">`)
|
|
40
|
+
// distingue os dois. Por isso a extração é por bloco (do início de um `<div
|
|
41
|
+
// class="result ` até o início do próximo), não por duas regexes globais pareadas por
|
|
42
|
+
// posição: título+snippet do mesmo bloco nunca desalinha com o resultado seguinte, e um
|
|
43
|
+
// bloco com `result--ad` na própria classe é descartado antes de extrair.
|
|
44
|
+
const BLOCK_START = '<div class="result ';
|
|
45
|
+
const startPositions = [];
|
|
46
|
+
for (let i = html.indexOf(BLOCK_START); i !== -1; i = html.indexOf(BLOCK_START, i + BLOCK_START.length)) {
|
|
47
|
+
startPositions.push(i);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const titleRe = /<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/;
|
|
51
|
+
const snippetRe = /<a[^>]*class="result__snippet"[^>]*href="[^"]+"[^>]*>([\s\S]*?)<\/a>/;
|
|
52
|
+
|
|
53
|
+
const results = [];
|
|
54
|
+
for (let i = 0; i < startPositions.length && results.length < num; i++) {
|
|
55
|
+
const start = startPositions[i];
|
|
56
|
+
const end = i + 1 < startPositions.length ? startPositions[i + 1] : html.length;
|
|
57
|
+
const block = html.slice(start, end);
|
|
58
|
+
|
|
59
|
+
const classEnd = block.indexOf('"', BLOCK_START.length);
|
|
60
|
+
const classAttr = block.slice(BLOCK_START.length, classEnd);
|
|
61
|
+
if (classAttr.split(/\s+/).includes('result--ad')) continue;
|
|
62
|
+
|
|
63
|
+
const titleMatch = titleRe.exec(block);
|
|
64
|
+
if (!titleMatch) continue;
|
|
65
|
+
const snippetMatch = snippetRe.exec(block);
|
|
66
|
+
|
|
67
|
+
results.push({
|
|
68
|
+
title: stripTags(titleMatch[2]),
|
|
69
|
+
url: realUrlFromDdgHref(titleMatch[1]),
|
|
70
|
+
snippet: snippetMatch ? stripTags(snippetMatch[1]) : '',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (results.length === 0) {
|
|
75
|
+
throw new Error('No result extracted: DuckDuckGo may have blocked or changed the HTML. Try again or rephrase the query.');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
process.stdout.write(JSON.stringify(results, null, 2));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Nunca chamar process.exit() aqui: com stdout/stderr em pipe (caso do
|
|
82
|
+
// subprocess.run que consome este script), abortar o processo enquanto ainda
|
|
83
|
+
// há handles assíncronos pendentes (ex.: o timer do AbortSignal.timeout do
|
|
84
|
+
// fetch) pode disparar um crash nativo do libuv no Windows. Só marcar o
|
|
85
|
+
// exit code e deixar o event loop drenar sozinho.
|
|
86
|
+
main().catch((err) => {
|
|
87
|
+
console.error(String(err?.message ?? err));
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
});
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Uso: node open-websearch-adapter.mjs "consulta" [--num 8] [--engine duckduckgo]
|
|
3
|
-
// Saída: JSON no stdout — [{ title, url, snippet }, ...]
|
|
4
|
-
// Wrapper fino sobre o pacote npm `open-websearch` (scraping multi-engine, sem chave de
|
|
5
|
-
// API) para expor o mesmo contrato dos demais adaptadores de web_search do Smart Tool
|
|
6
|
-
// (ver web_search_adapters.py no smart-tool) — o CLI do pacote devolve um envelope
|
|
7
|
-
// {data:{results:[...],partialFailures:[...]}} com campo `description`, não o array
|
|
8
|
-
// {title,url,snippet} que este contrato exige.
|
|
9
|
-
import { execFileSync } from 'node:child_process';
|
|
10
|
-
import { existsSync } from 'node:fs';
|
|
11
|
-
import path from 'node:path';
|
|
12
|
-
import { fileURLToPath } from 'node:url';
|
|
13
|
-
|
|
14
|
-
import { parseArgs } from './adapter-common.mjs';
|
|
15
|
-
|
|
16
|
-
// `npm root -g`/`npx` são `.cmd` no Windows -- `execFileSync` sem shell não invoca (EINVAL),
|
|
17
|
-
// e com shell:true o Node concatena argumentos sem escapar (DEP0190). O pacote fica
|
|
18
|
-
// instalado ao lado deste script por `install_runtime.py`; a chamada usa o arquivo JS
|
|
19
|
-
// diretamente com `process.execPath`. `OPEN_WEBSEARCH_CLI` permite diagnóstico local.
|
|
20
|
-
const LOCAL_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
const OPEN_WEBSEARCH_CLI = process.env.OPEN_WEBSEARCH_CLI || path.join(
|
|
22
|
-
LOCAL_DIR, 'node_modules', 'open-websearch', 'build', 'index.js',
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
function main() {
|
|
26
|
-
const { query, num, engine } = parseArgs(process.argv.slice(2),
|
|
27
|
-
'Usage: node open-websearch-adapter.mjs "query" [--num 8] [--engine name]', { num: 8, engine: 'duckduckgo' });
|
|
28
|
-
|
|
29
|
-
if (!existsSync(OPEN_WEBSEARCH_CLI)) {
|
|
30
|
-
throw new Error(`open-websearch is not installed (${OPEN_WEBSEARCH_CLI} does not exist); run: python install_runtime.py`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// O banner de log do pacote vai pro stderr (confirmado ao vivo); stdout fica só com o
|
|
34
|
-
// JSON do `--json`, então captar só stdout aqui não mistura texto solto com o parse.
|
|
35
|
-
let raw;
|
|
36
|
-
try {
|
|
37
|
-
raw = execFileSync(
|
|
38
|
-
process.execPath,
|
|
39
|
-
[OPEN_WEBSEARCH_CLI, 'search', query, '--engine', engine, '--limit', String(num), '--json'],
|
|
40
|
-
{ encoding: 'utf-8', timeout: 25000 },
|
|
41
|
-
);
|
|
42
|
-
} catch (err) {
|
|
43
|
-
const detail = (err.stderr || '').toString().trim();
|
|
44
|
-
throw new Error(detail || String(err.message || err));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const envelope = JSON.parse(raw);
|
|
48
|
-
if (envelope.error) {
|
|
49
|
-
throw new Error(String(envelope.error));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const results = (envelope.data?.results ?? []).slice(0, num).map((r) => ({
|
|
53
|
-
title: r.title ?? '',
|
|
54
|
-
url: r.url ?? '',
|
|
55
|
-
snippet: r.description ?? '',
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
|
-
if (results.length === 0) {
|
|
59
|
-
const failures = envelope.data?.partialFailures ?? [];
|
|
60
|
-
const reason = failures.length
|
|
61
|
-
? failures.map((f) => `${f.engine}: ${f.message}`).join('; ')
|
|
62
|
-
: 'no results';
|
|
63
|
-
throw new Error(`No result extracted (${engine}): ${reason}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
process.stdout.write(JSON.stringify(results));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Nunca process.exit() aqui: execFileSync já é síncrono (sem handle assíncrono pendente
|
|
70
|
-
// no momento do catch), mas o padrão do projeto (ver ddg-search.mjs) é sempre
|
|
71
|
-
// process.exitCode em vez de exit() com stdout/stderr em pipe.
|
|
72
|
-
try {
|
|
73
|
-
main();
|
|
74
|
-
} catch (err) {
|
|
75
|
-
console.error(String(err?.message ?? err));
|
|
76
|
-
process.exitCode = 1;
|
|
77
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Uso: node open-websearch-adapter.mjs "consulta" [--num 8] [--engine duckduckgo]
|
|
3
|
+
// Saída: JSON no stdout — [{ title, url, snippet }, ...]
|
|
4
|
+
// Wrapper fino sobre o pacote npm `open-websearch` (scraping multi-engine, sem chave de
|
|
5
|
+
// API) para expor o mesmo contrato dos demais adaptadores de web_search do Smart Tool
|
|
6
|
+
// (ver web_search_adapters.py no smart-tool) — o CLI do pacote devolve um envelope
|
|
7
|
+
// {data:{results:[...],partialFailures:[...]}} com campo `description`, não o array
|
|
8
|
+
// {title,url,snippet} que este contrato exige.
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
import { parseArgs } from './adapter-common.mjs';
|
|
15
|
+
|
|
16
|
+
// `npm root -g`/`npx` são `.cmd` no Windows -- `execFileSync` sem shell não invoca (EINVAL),
|
|
17
|
+
// e com shell:true o Node concatena argumentos sem escapar (DEP0190). O pacote fica
|
|
18
|
+
// instalado ao lado deste script por `install_runtime.py`; a chamada usa o arquivo JS
|
|
19
|
+
// diretamente com `process.execPath`. `OPEN_WEBSEARCH_CLI` permite diagnóstico local.
|
|
20
|
+
const LOCAL_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const OPEN_WEBSEARCH_CLI = process.env.OPEN_WEBSEARCH_CLI || path.join(
|
|
22
|
+
LOCAL_DIR, 'node_modules', 'open-websearch', 'build', 'index.js',
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function main() {
|
|
26
|
+
const { query, num, engine } = parseArgs(process.argv.slice(2),
|
|
27
|
+
'Usage: node open-websearch-adapter.mjs "query" [--num 8] [--engine name]', { num: 8, engine: 'duckduckgo' });
|
|
28
|
+
|
|
29
|
+
if (!existsSync(OPEN_WEBSEARCH_CLI)) {
|
|
30
|
+
throw new Error(`open-websearch is not installed (${OPEN_WEBSEARCH_CLI} does not exist); run: python install_runtime.py`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// O banner de log do pacote vai pro stderr (confirmado ao vivo); stdout fica só com o
|
|
34
|
+
// JSON do `--json`, então captar só stdout aqui não mistura texto solto com o parse.
|
|
35
|
+
let raw;
|
|
36
|
+
try {
|
|
37
|
+
raw = execFileSync(
|
|
38
|
+
process.execPath,
|
|
39
|
+
[OPEN_WEBSEARCH_CLI, 'search', query, '--engine', engine, '--limit', String(num), '--json'],
|
|
40
|
+
{ encoding: 'utf-8', timeout: 25000 },
|
|
41
|
+
);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const detail = (err.stderr || '').toString().trim();
|
|
44
|
+
throw new Error(detail || String(err.message || err));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const envelope = JSON.parse(raw);
|
|
48
|
+
if (envelope.error) {
|
|
49
|
+
throw new Error(String(envelope.error));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const results = (envelope.data?.results ?? []).slice(0, num).map((r) => ({
|
|
53
|
+
title: r.title ?? '',
|
|
54
|
+
url: r.url ?? '',
|
|
55
|
+
snippet: r.description ?? '',
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
if (results.length === 0) {
|
|
59
|
+
const failures = envelope.data?.partialFailures ?? [];
|
|
60
|
+
const reason = failures.length
|
|
61
|
+
? failures.map((f) => `${f.engine}: ${f.message}`).join('; ')
|
|
62
|
+
: 'no results';
|
|
63
|
+
throw new Error(`No result extracted (${engine}): ${reason}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
process.stdout.write(JSON.stringify(results));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Nunca process.exit() aqui: execFileSync já é síncrono (sem handle assíncrono pendente
|
|
70
|
+
// no momento do catch), mas o padrão do projeto (ver ddg-search.mjs) é sempre
|
|
71
|
+
// process.exitCode em vez de exit() com stdout/stderr em pipe.
|
|
72
|
+
try {
|
|
73
|
+
main();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error(String(err?.message ?? err));
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
{
|
|
2
|
-
"private": true,
|
|
3
|
-
"name": "smart-tool-web-adapters",
|
|
4
|
-
"version": "0.9.0",
|
|
5
|
-
"description": "Adaptadores Node empacotados com a Smart Tool",
|
|
6
|
-
"dependencies": {
|
|
7
|
-
"@angular/compiler": "20.3.0",
|
|
8
|
-
"java-parser": "3.0.1",
|
|
9
|
-
"open-websearch": "2.1.11",
|
|
10
|
-
"typescript": "5.9.3"
|
|
11
|
-
}
|
|
12
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"private": true,
|
|
3
|
+
"name": "smart-tool-web-adapters",
|
|
4
|
+
"version": "0.9.0",
|
|
5
|
+
"description": "Adaptadores Node empacotados com a Smart Tool",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@angular/compiler": "20.3.0",
|
|
8
|
+
"java-parser": "3.0.1",
|
|
9
|
+
"open-websearch": "2.1.11",
|
|
10
|
+
"typescript": "5.9.3"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Uso: node wikipedia-search.mjs "consulta" [--num 8] [--lang pt]
|
|
3
|
-
// Saída: JSON no stdout — [{ title, url, snippet }, ...]
|
|
4
|
-
// API oficial da Wikipedia (MediaWiki `action=query&list=search`), não scraping de SERP --
|
|
5
|
-
// fonte de índice genuinamente diferente dos demais adaptadores (enciclopédica, sem
|
|
6
|
-
// desafio anti-bot), mas só cobre conteúdo enciclopédico -- não serve pra cotação/notícia
|
|
7
|
-
// do dia, por exemplo.
|
|
8
|
-
import { parseArgs, stripTags } from './adapter-common.mjs';
|
|
9
|
-
|
|
10
|
-
async function main() {
|
|
11
|
-
const { query, num, lang } = parseArgs(process.argv.slice(2),
|
|
12
|
-
'Usage: node wikipedia-search.mjs "query" [--num 8] [--lang pt]', { num: 8, lang: 'pt' });
|
|
13
|
-
|
|
14
|
-
const url = `https://${lang}.wikipedia.org/w/api.php?` + new URLSearchParams({
|
|
15
|
-
action: 'query', list: 'search', format: 'json', srlimit: String(num), srsearch: query,
|
|
16
|
-
});
|
|
17
|
-
const res = await fetch(url, {
|
|
18
|
-
headers: { 'user-agent': process.env.SMART_TOOL_USER_AGENT || 'SmartTool (local MCP search tool)' },
|
|
19
|
-
signal: AbortSignal.timeout(15000),
|
|
20
|
-
});
|
|
21
|
-
if (!res.ok) {
|
|
22
|
-
throw new Error(`Wikipedia answered HTTP ${res.status}`);
|
|
23
|
-
}
|
|
24
|
-
const data = await res.json();
|
|
25
|
-
const hits = data?.query?.search ?? [];
|
|
26
|
-
if (hits.length === 0) {
|
|
27
|
-
throw new Error(`No Wikipedia (${lang}) result for this query.`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const results = hits.slice(0, num).map((h) => ({
|
|
31
|
-
title: h.title,
|
|
32
|
-
url: `https://${lang}.wikipedia.org/wiki/${encodeURIComponent(h.title.replace(/ /g, '_'))}`,
|
|
33
|
-
snippet: stripTags(h.snippet || ''),
|
|
34
|
-
}));
|
|
35
|
-
process.stdout.write(JSON.stringify(results));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Nunca process.exit() aqui: ver mesma nota em ddg-search.mjs (handle assíncrono do
|
|
39
|
-
// AbortSignal.timeout ainda pendente na hora de um exit() imediato crasharia o libuv
|
|
40
|
-
// no Windows) -- só marca o exit code e deixa o event loop drenar sozinho.
|
|
41
|
-
main().catch((err) => {
|
|
42
|
-
console.error(String(err?.message ?? err));
|
|
43
|
-
process.exitCode = 1;
|
|
44
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Uso: node wikipedia-search.mjs "consulta" [--num 8] [--lang pt]
|
|
3
|
+
// Saída: JSON no stdout — [{ title, url, snippet }, ...]
|
|
4
|
+
// API oficial da Wikipedia (MediaWiki `action=query&list=search`), não scraping de SERP --
|
|
5
|
+
// fonte de índice genuinamente diferente dos demais adaptadores (enciclopédica, sem
|
|
6
|
+
// desafio anti-bot), mas só cobre conteúdo enciclopédico -- não serve pra cotação/notícia
|
|
7
|
+
// do dia, por exemplo.
|
|
8
|
+
import { parseArgs, stripTags } from './adapter-common.mjs';
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const { query, num, lang } = parseArgs(process.argv.slice(2),
|
|
12
|
+
'Usage: node wikipedia-search.mjs "query" [--num 8] [--lang pt]', { num: 8, lang: 'pt' });
|
|
13
|
+
|
|
14
|
+
const url = `https://${lang}.wikipedia.org/w/api.php?` + new URLSearchParams({
|
|
15
|
+
action: 'query', list: 'search', format: 'json', srlimit: String(num), srsearch: query,
|
|
16
|
+
});
|
|
17
|
+
const res = await fetch(url, {
|
|
18
|
+
headers: { 'user-agent': process.env.SMART_TOOL_USER_AGENT || 'SmartTool (local MCP search tool)' },
|
|
19
|
+
signal: AbortSignal.timeout(15000),
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
throw new Error(`Wikipedia answered HTTP ${res.status}`);
|
|
23
|
+
}
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
const hits = data?.query?.search ?? [];
|
|
26
|
+
if (hits.length === 0) {
|
|
27
|
+
throw new Error(`No Wikipedia (${lang}) result for this query.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const results = hits.slice(0, num).map((h) => ({
|
|
31
|
+
title: h.title,
|
|
32
|
+
url: `https://${lang}.wikipedia.org/wiki/${encodeURIComponent(h.title.replace(/ /g, '_'))}`,
|
|
33
|
+
snippet: stripTags(h.snippet || ''),
|
|
34
|
+
}));
|
|
35
|
+
process.stdout.write(JSON.stringify(results));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Nunca process.exit() aqui: ver mesma nota em ddg-search.mjs (handle assíncrono do
|
|
39
|
+
// AbortSignal.timeout ainda pendente na hora de um exit() imediato crasharia o libuv
|
|
40
|
+
// no Windows) -- só marca o exit code e deixa o event loop drenar sozinho.
|
|
41
|
+
main().catch((err) => {
|
|
42
|
+
console.error(String(err?.message ?? err));
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
});
|