argo-search 2.9.7 → 2.9.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.en.md +73 -150
- package/README.es.md +73 -150
- package/README.ja.md +79 -156
- package/README.ko.md +78 -155
- package/README.md +80 -159
- package/SKILL.md +3 -3
- package/assets/readme/hero.svg +2 -2
- package/backends/domain_profiles.json +48 -0
- package/backends/engine_registry.yaml +118 -4
- package/backends/quota_profiles.json +92 -12
- package/backends/source_types_cn.json +27 -1
- package/bin/argo +78 -5
- package/bin/argo.js +33 -2
- package/config.yaml +269 -63
- package/docs/ARGO_INTRO.md +5 -3
- package/docs/ENGINE_CATALOG.md +50 -39
- package/docs/MCP_SETUP.md +3 -1
- package/docs/RELEASE_NOTES_v2.9.8.md +73 -0
- package/docs/RELEASE_NOTES_v2.9.9.md +78 -0
- package/engines/specs/cfets.yaml +41 -0
- package/engines/specs/defillama.yaml +38 -0
- package/engines/specs/firecrawl.yaml +1 -1
- package/engines/specs/fxtwitter.yaml +5 -0
- package/engines/specs/gleif.yaml +55 -0
- package/engines/specs/hkex.yaml +40 -0
- package/engines/specs/keenable_free.yaml +36 -0
- package/engines/specs/shfe.yaml +39 -0
- package/engines/specs/stackexchange.yaml +2 -2
- package/engines/specs/tavily_free.yaml +56 -0
- package/engines/specs/yuanbao.yaml +93 -0
- package/mcp/clients.yaml +32 -0
- package/package.json +7 -2
- package/packages/dsh-plugin/README.md +25 -6
- package/packages/dsh-plugin/dsh/client.js +402 -47
- package/packages/dsh-plugin/dsh/diagnostics.js +97 -0
- package/packages/dsh-plugin/dsh/index.js +135 -1876
- package/packages/dsh-plugin/dsh/native-tools.mjs +7 -1
- package/packages/dsh-plugin/dsh/runner.js +666 -0
- package/packages/dsh-plugin/dsh/wide-research.js +878 -0
- package/packages/dsh-plugin/package.json +3 -3
- package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
- package/packages/dsh-plugin/test/client.test.js +177 -2
- package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
- package/packages/dsh-plugin/test/index.test.js +44 -0
- package/scripts/adaptive.py +126 -43
- package/scripts/article_meta.py +131 -0
- package/scripts/bench_tail_ab.py +281 -0
- package/scripts/bounded_run.py +91 -49
- package/scripts/budget.py +247 -0
- package/scripts/budget_cli.py +93 -0
- package/scripts/build_release_assets.sh +65 -0
- package/scripts/cache.py +60 -72
- package/scripts/ci_drift_gate.py +90 -0
- package/scripts/clock_bridge.py +22 -0
- package/scripts/content_boundary.py +46 -0
- package/scripts/content_security.py +119 -4
- package/scripts/content_signals.py +26 -5
- package/scripts/cross_lingual_trace.py +293 -0
- package/scripts/dispatch_result.py +105 -0
- package/scripts/engine_add.py +602 -0
- package/scripts/engine_dispatch.py +58 -86
- package/scripts/engine_env.py +5 -0
- package/scripts/engine_families.py +6 -0
- package/scripts/engine_policy.py +7 -7
- package/scripts/engines.py +21 -0
- package/scripts/engines_base.py +3 -3
- package/scripts/engines_builders.py +12 -0
- package/scripts/engines_builders_batch10.py +48 -3
- package/scripts/engines_builders_batch14.py +656 -0
- package/scripts/engines_builders_batch15.py +249 -0
- package/scripts/engines_builders_keyless.py +111 -0
- package/scripts/engines_builders_tech.py +0 -1
- package/scripts/evidence.py +31 -3
- package/scripts/evidence_loop.py +58 -22
- package/scripts/fetch_quality.py +13 -0
- package/scripts/fetch_v3.py +28 -3
- package/scripts/gen_routing_snapshot.py +11 -0
- package/scripts/http_client.py +24 -38
- package/scripts/install.ps1 +18 -0
- package/scripts/install.sh +16 -1
- package/scripts/job.py +3 -3
- package/scripts/local_seek.py +41 -5
- package/scripts/macro_countries.py +15 -0
- package/scripts/matrix_search_eval.py +192 -0
- package/scripts/mcp_compact.py +234 -0
- package/scripts/mcp_handlers.py +60 -642
- package/scripts/mcp_launch.sh +10 -8
- package/scripts/mcp_local.py +241 -0
- package/scripts/mcp_server.py +19 -14
- package/scripts/mcp_setup.py +33 -9
- package/scripts/mcp_tools.py +1 -0
- package/scripts/mcp_transport.py +1 -1
- package/scripts/mcp_warm.py +127 -0
- package/scripts/net_proxy.py +27 -0
- package/scripts/output_transparency.py +200 -0
- package/scripts/paid_policy.py +98 -0
- package/scripts/quota.py +158 -13
- package/scripts/recovery.py +7 -3
- package/scripts/recovery_runner.py +150 -0
- package/scripts/replay_eval.py +18 -0
- package/scripts/research.py +37 -21
- package/scripts/research_expand.py +24 -1
- package/scripts/retry_policy.py +76 -0
- package/scripts/route.py +6 -6
- package/scripts/route_cache.py +18 -0
- package/scripts/route_combo.py +26 -5
- package/scripts/route_domains.py +170 -31
- package/scripts/safe_conn.py +52 -0
- package/scripts/search.py +15 -27
- package/scripts/search_attribution.py +100 -3
- package/scripts/search_cli.py +24 -11
- package/scripts/search_entry.py +8 -1
- package/scripts/search_output.py +99 -15
- package/scripts/search_pipeline.py +62 -195
- package/scripts/search_rank.py +6 -0
- package/scripts/search_types.py +107 -9
- package/scripts/semantic_evidence.py +179 -3
- package/scripts/serp_guard.py +138 -7
- package/scripts/serp_kill_log.py +25 -2
- package/scripts/stats_cli.py +13 -1
- package/scripts/tail_completion.py +212 -0
- package/scripts/url_safety.py +126 -3
- package/scripts/usage_log.py +45 -0
- package/scripts/wide_research.py +45 -0
- package/scripts/zhihu_quota.py +139 -0
- package/sub-skills/local-search/bm25.py +27 -3
- package/sub-skills/local-search/cli_runner.py +331 -0
- package/sub-skills/local-search/parsers.py +328 -0
- package/sub-skills/local-search/search_v3.py +31 -674
- package/sub-skills/local-seek/scripts/seek_rank.py +7 -3
- package/assets/readme/settings-keys.png +0 -0
- package/assets/readme/settings-tools.png +0 -0
- package/sub-skills/ego-search/references/browser-runtime.md +0 -43
- package/sub-skills/ego-search/references/install.md +0 -67
- package/sub-skills/ego-search/references/original-ego-upgrade.md +0 -51
- package/sub-skills/ego-search/tests/test_query_normalize.py +0 -68
- package/sub-skills/js-run/tests/fixtures/challenge_like.js +0 -27
- package/sub-skills/js-run/tests/test_jsrun.py +0 -165
- package/sub-skills/local-seek/references/auto-behaviors.md +0 -24
- package/sub-skills/local-seek/references/fd-mdfind-recipes.md +0 -62
- package/sub-skills/local-seek/references/git-integration.md +0 -54
- package/sub-skills/local-seek/references/remote-handoff.md +0 -46
- package/sub-skills/local-seek/references/rg-recipes.md +0 -96
- package/sub-skills/local-seek/references/strategies.md +0 -52
- package/sub-skills/local-seek/references/structural-search.md +0 -59
- package/sub-skills/local-seek/tests/test_content_rank.py +0 -222
- package/sub-skills/local-seek/tests/test_determinism_20261006.py +0 -145
- package/sub-skills/local-seek/tests/test_noise_tier.py +0 -101
- package/sub-skills/local-seek/tests/test_pcre2_probe.py +0 -136
- package/sub-skills/local-seek/tests/test_rank_pinyin.py +0 -77
- package/sub-skills/local-seek/tests/test_regression_20261003.py +0 -169
- package/sub-skills/local-seek/tests/test_regression_20261006.py +0 -146
- package/sub-skills/local-seek/tests/test_seek_degradation.py +0 -111
- package/sub-skills/local-seek/tests/test_seek_fts.py +0 -313
|
@@ -375,6 +375,11 @@ export const NATIVE_TOOLS = {
|
|
|
375
375
|
"type": "boolean",
|
|
376
376
|
"description": "强制用反检测浏览器(HTTP 失败时已自动升级)",
|
|
377
377
|
"default": false
|
|
378
|
+
},
|
|
379
|
+
"include_links": {
|
|
380
|
+
"type": "boolean",
|
|
381
|
+
"description": "附带正文内链接(最多 30 条;默认只回 links_count,省 token)",
|
|
382
|
+
"default": false
|
|
378
383
|
}
|
|
379
384
|
},
|
|
380
385
|
"required": [
|
|
@@ -388,7 +393,8 @@ export const NATIVE_TOOLS = {
|
|
|
388
393
|
"extract_mode",
|
|
389
394
|
"focus",
|
|
390
395
|
"max_chars",
|
|
391
|
-
"use_browser"
|
|
396
|
+
"use_browser",
|
|
397
|
+
"include_links"
|
|
392
398
|
]
|
|
393
399
|
},
|
|
394
400
|
"argo_screenshot": {
|
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* runner.js — argo-dsh 执行与连接池层(Runner & Connection Pool)
|
|
3
|
+
*
|
|
4
|
+
* 负责:
|
|
5
|
+
* 1. 常驻 stdio MCP 连接池(按需增长、并发独立、空闲回收、零泄漏)
|
|
6
|
+
* 2. 原生一等工具(makeNativeTool / runNativeCall)单发兜底与 MCP 优先调度
|
|
7
|
+
* 3. 宿主 web_search & web_fetch seam 结果映射与错误分类
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn } from 'node:child_process'
|
|
11
|
+
import { readFileSync } from 'node:fs'
|
|
12
|
+
import { homedir } from 'node:os'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
import { NATIVE_TOOLS as NATIVE_TOOL_SPECS } from './native-tools.mjs'
|
|
16
|
+
|
|
17
|
+
function resolvePluginVersion() {
|
|
18
|
+
try {
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
const raw = readFileSync(join(here, '..', 'package.json'), 'utf8')
|
|
21
|
+
const parsed = JSON.parse(raw)
|
|
22
|
+
if (typeof parsed?.version === 'string' && parsed.version !== '') return parsed.version
|
|
23
|
+
} catch { /* 打包裁剪/权限异常:回退,不阻断插件加载 */ }
|
|
24
|
+
return '0.0.0-unknown'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const PLUGIN_VERSION = resolvePluginVersion()
|
|
28
|
+
|
|
29
|
+
export const MCP_CHILD_TOOLS = Object.keys(NATIVE_TOOL_SPECS).map(name => `mcp__argo__${name}`)
|
|
30
|
+
export const DEFAULT_CHILD_TOOLS = Object.freeze([
|
|
31
|
+
...MCP_CHILD_TOOLS,
|
|
32
|
+
'argo_search',
|
|
33
|
+
'argo_fetch',
|
|
34
|
+
'web_search',
|
|
35
|
+
'web_fetch',
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
export const DEFAULTS = Object.freeze({
|
|
39
|
+
toolName: 'wide_research',
|
|
40
|
+
provider: 'spawn',
|
|
41
|
+
defaultWorkers: 6,
|
|
42
|
+
maxWorkers: 9,
|
|
43
|
+
maxTracks: 9,
|
|
44
|
+
maxSourcesPerTrack: 5,
|
|
45
|
+
workerMaxTokens: 5_000,
|
|
46
|
+
synthesisMaxTokens: 7_000,
|
|
47
|
+
childToolDeny: [],
|
|
48
|
+
childToolAllow: [...DEFAULT_CHILD_TOOLS],
|
|
49
|
+
searchProviderEnabled: true,
|
|
50
|
+
nativeTools: ['argo_search', 'argo_fetch'],
|
|
51
|
+
nativeTimeoutMs: 60_000,
|
|
52
|
+
nativeMaxChars: 24_000,
|
|
53
|
+
searchProviderId: 'argo',
|
|
54
|
+
fetchProviderEnabled: true,
|
|
55
|
+
fetchProviderId: 'argo',
|
|
56
|
+
fetchTimeoutMs: 25_000,
|
|
57
|
+
fetchMaxChars: 50_000,
|
|
58
|
+
searchCommand: process.env.ARGO_SEARCH_PYTHON || 'npx',
|
|
59
|
+
searchArgs: process.env.ARGO_SEARCH_MCP_SERVER
|
|
60
|
+
? [process.env.ARGO_SEARCH_MCP_SERVER]
|
|
61
|
+
: ['-y', 'github:taxueseek/argo'],
|
|
62
|
+
searchTimeoutMs: 30_000,
|
|
63
|
+
searchIdleMs: 60_000,
|
|
64
|
+
searchConcurrency: 3,
|
|
65
|
+
reportDir: join(homedir(), '.dsh-research'),
|
|
66
|
+
keyRefs: ['DEEPSEEK_API_KEY'],
|
|
67
|
+
argoEnv: [],
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const text = (value) => [{ type: 'text', text: value }]
|
|
71
|
+
const clip = (value, maximum) => value.length <= maximum ? value : `${value.slice(0, maximum - 1)}…`
|
|
72
|
+
const clamp = (value, fallback, minimum, maximum) => Number.isFinite(value)
|
|
73
|
+
? Math.max(minimum, Math.min(maximum, Math.floor(value)))
|
|
74
|
+
: fallback
|
|
75
|
+
|
|
76
|
+
export function argoAborted() {
|
|
77
|
+
const err = new Error('argo search aborted')
|
|
78
|
+
err.code = 'WEB_ABORTED'
|
|
79
|
+
return err
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function argoToolError(message) {
|
|
83
|
+
const err = new Error(message)
|
|
84
|
+
err.code = 'ARGO_TOOL_ERROR'
|
|
85
|
+
return err
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const stripBoundary = (v) => typeof v === 'string'
|
|
89
|
+
? v.replace(/<\s*\/?\s*untrusted-web-content\b[^>]*>/gi, '')
|
|
90
|
+
: v
|
|
91
|
+
|
|
92
|
+
export function mapArgoToWebResult(payload, query) {
|
|
93
|
+
const results = Array.isArray(payload?.results)
|
|
94
|
+
? payload.results.filter(r => r && typeof r.url === 'string' && r.url !== '')
|
|
95
|
+
: []
|
|
96
|
+
const sources = results.map(r => {
|
|
97
|
+
const source = { url: r.url }
|
|
98
|
+
const title = stripBoundary(r.title)
|
|
99
|
+
const snippet = stripBoundary(r.snippet)
|
|
100
|
+
if (typeof title === 'string' && title !== '') source.title = title
|
|
101
|
+
if (typeof snippet === 'string' && snippet !== '') source.snippet = snippet
|
|
102
|
+
return source
|
|
103
|
+
})
|
|
104
|
+
return { content: undefined, sources, truncated: false, engine: payload?.engine ?? 'argo' }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let activeEnvResolver = null
|
|
108
|
+
|
|
109
|
+
export function setEnvResolver(fn) {
|
|
110
|
+
activeEnvResolver = fn
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function resolveEnv(options = {}, config = undefined) {
|
|
114
|
+
if (typeof activeEnvResolver === 'function') {
|
|
115
|
+
return activeEnvResolver(options, config)
|
|
116
|
+
}
|
|
117
|
+
return undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const mcpPool = []
|
|
121
|
+
|
|
122
|
+
export function dropFromPool(conn) {
|
|
123
|
+
const index = mcpPool.indexOf(conn)
|
|
124
|
+
if (index >= 0) mcpPool.splice(index, 1)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function createMcpConnection(options) {
|
|
128
|
+
const { command, args, idleMs, env } = options
|
|
129
|
+
let proc = null
|
|
130
|
+
let buffer = ''
|
|
131
|
+
let nextId = 1
|
|
132
|
+
let chain = Promise.resolve()
|
|
133
|
+
let idleTimer = null
|
|
134
|
+
let disposed = false
|
|
135
|
+
let claims = 0
|
|
136
|
+
const pending = new Map()
|
|
137
|
+
|
|
138
|
+
const touchIdle = () => {
|
|
139
|
+
if (idleTimer !== null) clearTimeout(idleTimer)
|
|
140
|
+
if (idleMs > 0) {
|
|
141
|
+
idleTimer = setTimeout(() => { close() }, idleMs)
|
|
142
|
+
if (idleTimer.unref) idleTimer.unref()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const close = () => {
|
|
147
|
+
if (disposed) return
|
|
148
|
+
disposed = true
|
|
149
|
+
if (idleTimer !== null) clearTimeout(idleTimer)
|
|
150
|
+
if (proc !== null) {
|
|
151
|
+
try { proc.kill() } catch { /* already gone */ }
|
|
152
|
+
proc = null
|
|
153
|
+
}
|
|
154
|
+
const err = new Error('argo MCP connection closed')
|
|
155
|
+
for (const entry of pending.values()) entry.rej(err)
|
|
156
|
+
pending.clear()
|
|
157
|
+
dropFromPool(conn)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const send = (method, params, timeoutMs = 0) => {
|
|
161
|
+
touchIdle()
|
|
162
|
+
const run = () => new Promise((res, rej) => {
|
|
163
|
+
if (proc === null || proc.stdin.destroyed) {
|
|
164
|
+
rej(new Error('argo MCP process not running'))
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
const id = nextId
|
|
168
|
+
nextId += 1
|
|
169
|
+
const entry = { res, rej }
|
|
170
|
+
pending.set(id, entry)
|
|
171
|
+
|
|
172
|
+
let reqTimer = null
|
|
173
|
+
if (timeoutMs > 0) {
|
|
174
|
+
reqTimer = setTimeout(() => {
|
|
175
|
+
pending.delete(id)
|
|
176
|
+
const err = new Error(`argo MCP request timed out after ${timeoutMs}ms: ${method}`)
|
|
177
|
+
err.code = 'ARGO_TIMEOUT'
|
|
178
|
+
rej(err)
|
|
179
|
+
close()
|
|
180
|
+
}, timeoutMs)
|
|
181
|
+
if (reqTimer.unref) reqTimer.unref()
|
|
182
|
+
}
|
|
183
|
+
const settle = (fn, value) => {
|
|
184
|
+
if (reqTimer !== null) clearTimeout(reqTimer)
|
|
185
|
+
fn(value)
|
|
186
|
+
}
|
|
187
|
+
entry.res = (v) => settle(res, v)
|
|
188
|
+
entry.rej = (e) => settle(rej, e)
|
|
189
|
+
try {
|
|
190
|
+
proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n')
|
|
191
|
+
} catch (err) {
|
|
192
|
+
pending.delete(id)
|
|
193
|
+
rej(err)
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
const result = chain.then(run)
|
|
197
|
+
chain = result.catch(() => { /* 失败不中断后续请求 */ })
|
|
198
|
+
return result
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const conn = {
|
|
202
|
+
initialize: async (timeoutMs = 0) => {
|
|
203
|
+
if (proc !== null) return
|
|
204
|
+
proc = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], env: env ?? process.env })
|
|
205
|
+
proc.on('error', () => close())
|
|
206
|
+
proc.on('exit', () => close())
|
|
207
|
+
proc.stderr.on('data', () => { /* 预热日志忽略 */ })
|
|
208
|
+
proc.stdout.setEncoding('utf8')
|
|
209
|
+
proc.stdout.on('data', (chunk) => {
|
|
210
|
+
buffer += chunk
|
|
211
|
+
let nl
|
|
212
|
+
while ((nl = buffer.indexOf('\n')) >= 0) {
|
|
213
|
+
const line = buffer.slice(0, nl).trim()
|
|
214
|
+
buffer = buffer.slice(nl + 1)
|
|
215
|
+
if (line === '') continue
|
|
216
|
+
let msg
|
|
217
|
+
try {
|
|
218
|
+
msg = JSON.parse(line)
|
|
219
|
+
} catch {
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
const entry = pending.get(msg.id)
|
|
223
|
+
if (entry === undefined) continue
|
|
224
|
+
pending.delete(msg.id)
|
|
225
|
+
if (msg.error !== undefined) entry.rej(new Error(msg.error.message ?? 'argo MCP error'))
|
|
226
|
+
else entry.res(msg.result)
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
await new Promise((res, rej) => {
|
|
230
|
+
proc.once('spawn', res)
|
|
231
|
+
proc.once('error', rej)
|
|
232
|
+
})
|
|
233
|
+
const init = await send('initialize', {
|
|
234
|
+
protocolVersion: '2025-06-18',
|
|
235
|
+
capabilities: {},
|
|
236
|
+
clientInfo: { name: 'argo-dsh', version: PLUGIN_VERSION }
|
|
237
|
+
}, timeoutMs)
|
|
238
|
+
conn.notify('notifications/initialized', {})
|
|
239
|
+
return init
|
|
240
|
+
},
|
|
241
|
+
request: (method, params, timeoutMs = 0) => {
|
|
242
|
+
if (claims > 0) claims -= 1
|
|
243
|
+
return send(method, params, timeoutMs)
|
|
244
|
+
},
|
|
245
|
+
claim: () => {
|
|
246
|
+
claims += 1
|
|
247
|
+
touchIdle()
|
|
248
|
+
},
|
|
249
|
+
release: () => {
|
|
250
|
+
if (claims > 0) claims -= 1
|
|
251
|
+
touchIdle()
|
|
252
|
+
},
|
|
253
|
+
load: () => pending.size + claims,
|
|
254
|
+
notify: (method, params) => {
|
|
255
|
+
if (proc !== null && !proc.stdin.destroyed) {
|
|
256
|
+
try {
|
|
257
|
+
proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n')
|
|
258
|
+
} catch { /* ignore */ }
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
close
|
|
262
|
+
}
|
|
263
|
+
return conn
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function initializeWithSignal(conn, initMs, signal) {
|
|
267
|
+
if (signal?.aborted) {
|
|
268
|
+
conn.close()
|
|
269
|
+
return Promise.reject(argoAborted())
|
|
270
|
+
}
|
|
271
|
+
if (signal === undefined) return conn.initialize(initMs)
|
|
272
|
+
return new Promise((resolve, reject) => {
|
|
273
|
+
const onAbort = () => {
|
|
274
|
+
conn.close()
|
|
275
|
+
reject(argoAborted())
|
|
276
|
+
}
|
|
277
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
278
|
+
conn.initialize(initMs).then(
|
|
279
|
+
(value) => { signal.removeEventListener('abort', onAbort); resolve(value) },
|
|
280
|
+
(err) => { signal.removeEventListener('abort', onAbort); reject(err) },
|
|
281
|
+
)
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function acquireMcp(options, signal) {
|
|
286
|
+
const max = Math.max(1, Number(options.max) || DEFAULTS.searchConcurrency)
|
|
287
|
+
const initMs = Math.max(0, Number(options.initMs) || 0)
|
|
288
|
+
const idle = mcpPool.find(entry => entry.load() === 0)
|
|
289
|
+
if (idle !== undefined) {
|
|
290
|
+
idle.claim()
|
|
291
|
+
try {
|
|
292
|
+
await initializeWithSignal(idle, initMs, signal)
|
|
293
|
+
return idle
|
|
294
|
+
} catch (err) {
|
|
295
|
+
dropFromPool(idle)
|
|
296
|
+
idle.close()
|
|
297
|
+
if (mcpPool.length >= max) throw err
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (mcpPool.length < max) {
|
|
301
|
+
const env = await resolveEnv(options)
|
|
302
|
+
const conn = createMcpConnection({ ...options, env })
|
|
303
|
+
mcpPool.push(conn)
|
|
304
|
+
conn.claim()
|
|
305
|
+
try {
|
|
306
|
+
await initializeWithSignal(conn, initMs, signal)
|
|
307
|
+
} catch (err) {
|
|
308
|
+
dropFromPool(conn)
|
|
309
|
+
conn.close()
|
|
310
|
+
throw err
|
|
311
|
+
}
|
|
312
|
+
return conn
|
|
313
|
+
}
|
|
314
|
+
const least = mcpPool.reduce((a, b) => (a.load() <= b.load() ? a : b))
|
|
315
|
+
least.claim()
|
|
316
|
+
try {
|
|
317
|
+
await initializeWithSignal(least, initMs, signal)
|
|
318
|
+
} catch (err) {
|
|
319
|
+
dropFromPool(least)
|
|
320
|
+
least.close()
|
|
321
|
+
throw err
|
|
322
|
+
}
|
|
323
|
+
return least
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function disposeSharedMcp() {
|
|
327
|
+
for (const conn of [...mcpPool]) conn.close()
|
|
328
|
+
mcpPool.length = 0
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function classifyFetchError(err, url) {
|
|
332
|
+
const raw = String(err?.message ?? err ?? '')
|
|
333
|
+
const lower = raw.toLowerCase()
|
|
334
|
+
const prefix = (label, hint) => {
|
|
335
|
+
const e = new Error(`argo fetch ${label}: ${hint} (url=${url})`)
|
|
336
|
+
e.code = `ARGO_FETCH_${label.toUpperCase().replace(/-/g, '_')}`
|
|
337
|
+
return e
|
|
338
|
+
}
|
|
339
|
+
if (err?.code === 'WEB_ABORTED' || /abort/i.test(raw)) {
|
|
340
|
+
return prefix('aborted', 'cancelled by caller')
|
|
341
|
+
}
|
|
342
|
+
if (err?.code === 'ARGO_TIMEOUT' || /timed out|timeout/i.test(lower)) {
|
|
343
|
+
return prefix('timeout', 'the page took too long; try a more specific URL or retry')
|
|
344
|
+
}
|
|
345
|
+
if (/blocked|forbidden|403|robots|captcha|cloudflare|denied/i.test(lower)) {
|
|
346
|
+
return prefix('blocked', 'the source refused automated access; use a search result URL instead')
|
|
347
|
+
}
|
|
348
|
+
if (/no readable|no content|nothing extracted|no results|empty/i.test(lower)) {
|
|
349
|
+
return prefix('empty', 'no readable content was extracted from the page')
|
|
350
|
+
}
|
|
351
|
+
const e = new Error(`argo fetch failed: ${clip(raw, 300)} (url=${url})`)
|
|
352
|
+
e.code = 'ARGO_FETCH_FAILED'
|
|
353
|
+
return e
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function mapArgoToWebFetch(payload, requestUrl, maxChars) {
|
|
357
|
+
const url = typeof payload?.url === 'string' && payload.url !== '' ? payload.url : requestUrl
|
|
358
|
+
const content = typeof payload?.content === 'string' ? stripBoundary(payload.content) : ''
|
|
359
|
+
const clipped = content.length > maxChars
|
|
360
|
+
return {
|
|
361
|
+
url,
|
|
362
|
+
statusCode: 200,
|
|
363
|
+
body: { kind: 'text', content: clipped ? content.slice(0, maxChars) : content },
|
|
364
|
+
truncated: Boolean(payload?.truncated) || clipped,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function fetchViaArgo(url, signal, options = {}, deps = {}) {
|
|
369
|
+
const command = options.command ?? DEFAULTS.searchCommand
|
|
370
|
+
const args = options.args ?? DEFAULTS.searchArgs
|
|
371
|
+
const timeoutMs = options.timeoutMs ?? DEFAULTS.fetchTimeoutMs
|
|
372
|
+
const maxChars = clamp(options.maxChars ?? DEFAULTS.fetchMaxChars,
|
|
373
|
+
DEFAULTS.fetchMaxChars, 500, 50_000)
|
|
374
|
+
const argsObj = { url, max_chars: maxChars }
|
|
375
|
+
const spawnRun = deps.run ?? runNativeCall
|
|
376
|
+
|
|
377
|
+
const acquireOptions = {
|
|
378
|
+
command,
|
|
379
|
+
args,
|
|
380
|
+
idleMs: options.idleMs ?? DEFAULTS.searchIdleMs,
|
|
381
|
+
max: options.max ?? DEFAULTS.searchConcurrency,
|
|
382
|
+
initMs: timeoutMs,
|
|
383
|
+
credentials: options.credentials,
|
|
384
|
+
config: options.config,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
let raw
|
|
388
|
+
try {
|
|
389
|
+
const conn = await acquireMcp(acquireOptions, signal)
|
|
390
|
+
raw = await new Promise((resolve, reject) => {
|
|
391
|
+
const onAbort = () => reject(argoAborted())
|
|
392
|
+
if (signal?.aborted) {
|
|
393
|
+
conn.release()
|
|
394
|
+
reject(argoAborted())
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
if (signal !== undefined) signal.addEventListener('abort', onAbort, { once: true })
|
|
398
|
+
conn.request('tools/call', { name: 'argo_fetch', arguments: argsObj }, timeoutMs).then((value) => {
|
|
399
|
+
if (signal !== undefined) signal.removeEventListener('abort', onAbort)
|
|
400
|
+
if (value?.isError) {
|
|
401
|
+
const body = value?.content?.[0]?.text
|
|
402
|
+
reject(argoToolError(typeof body === 'string' && body !== ''
|
|
403
|
+
? body
|
|
404
|
+
: `argo tool argo_fetch returned an error`))
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
const text = value?.content?.[0]?.text
|
|
408
|
+
resolve(typeof text === 'string' ? JSON.parse(text) : {})
|
|
409
|
+
}, (err) => {
|
|
410
|
+
if (signal !== undefined) signal.removeEventListener('abort', onAbort)
|
|
411
|
+
reject(err)
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
} catch (err) {
|
|
415
|
+
if (err?.code === 'ARGO_TOOL_ERROR' || err?.code === 'ARGO_TIMEOUT' || err?.code === 'WEB_ABORTED') {
|
|
416
|
+
throw classifyFetchError(err, url)
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
const spawnSpec = resolveNativeSpawn(
|
|
420
|
+
{ searchCommand: command, searchArgs: args },
|
|
421
|
+
'argo_fetch',
|
|
422
|
+
JSON.stringify(argsObj),
|
|
423
|
+
)
|
|
424
|
+
const env = await resolveEnv(options)
|
|
425
|
+
const stdout = await spawnRun(env !== undefined ? { ...spawnSpec, env } : spawnSpec, timeoutMs, signal)
|
|
426
|
+
const parsed = JSON.parse(stdout)
|
|
427
|
+
if (parsed?.isError) {
|
|
428
|
+
const body = parsed?.content?.[0]?.text
|
|
429
|
+
throw argoToolError(typeof body === 'string' && body !== '' ? body : 'argo_fetch failed')
|
|
430
|
+
}
|
|
431
|
+
raw = JSON.parse(parsed?.content?.[0]?.text ?? '{}')
|
|
432
|
+
} catch (spawnErr) {
|
|
433
|
+
throw classifyFetchError(spawnErr, url)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (raw?.success === false) {
|
|
438
|
+
throw classifyFetchError(new Error(String(raw?.error ?? 'fetch failed')), url)
|
|
439
|
+
}
|
|
440
|
+
return mapArgoToWebFetch(raw, url, maxChars)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export async function searchViaArgoMCP(query, maxResults = 5, signal, options = {}) {
|
|
444
|
+
const command = options.command ?? DEFAULTS.searchCommand
|
|
445
|
+
const args = options.args ?? DEFAULTS.searchArgs
|
|
446
|
+
const timeoutMs = options.timeoutMs ?? DEFAULTS.searchTimeoutMs
|
|
447
|
+
const count = clamp(maxResults ?? 5, 5, 1, 20)
|
|
448
|
+
|
|
449
|
+
const conn = await acquireMcp({
|
|
450
|
+
command,
|
|
451
|
+
args,
|
|
452
|
+
idleMs: options.idleMs ?? DEFAULTS.searchIdleMs,
|
|
453
|
+
max: options.max ?? DEFAULTS.searchConcurrency,
|
|
454
|
+
initMs: timeoutMs,
|
|
455
|
+
credentials: options.credentials,
|
|
456
|
+
config: options.config,
|
|
457
|
+
}, signal)
|
|
458
|
+
|
|
459
|
+
const result = await new Promise((resolve, reject) => {
|
|
460
|
+
const onAbort = () => reject(argoAborted())
|
|
461
|
+
if (signal !== undefined) {
|
|
462
|
+
if (signal.aborted) {
|
|
463
|
+
conn.release()
|
|
464
|
+
reject(argoAborted())
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
468
|
+
}
|
|
469
|
+
conn.request('tools/call', {
|
|
470
|
+
name: 'argo_search',
|
|
471
|
+
arguments: { query, max_results: count, summary: true }
|
|
472
|
+
}, timeoutMs).then((value) => {
|
|
473
|
+
if (signal !== undefined) signal.removeEventListener('abort', onAbort)
|
|
474
|
+
resolve(value)
|
|
475
|
+
}, (err) => {
|
|
476
|
+
if (signal !== undefined) signal.removeEventListener('abort', onAbort)
|
|
477
|
+
reject(err)
|
|
478
|
+
})
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
let payload
|
|
482
|
+
try {
|
|
483
|
+
const raw = result?.content?.[0]?.text
|
|
484
|
+
payload = typeof raw === 'string' ? JSON.parse(raw) : {}
|
|
485
|
+
} catch (err) {
|
|
486
|
+
throw new Error(`argo search returned an unprocessable response: ${String(err)}`)
|
|
487
|
+
}
|
|
488
|
+
return mapArgoToWebResult(payload, query)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function resolveNativeSpawn(config, tool, payload) {
|
|
492
|
+
const command = typeof config.searchCommand === 'string' && config.searchCommand !== ''
|
|
493
|
+
? config.searchCommand
|
|
494
|
+
: DEFAULTS.searchCommand
|
|
495
|
+
const args = Array.isArray(config.searchArgs) && config.searchArgs.length > 0
|
|
496
|
+
? config.searchArgs
|
|
497
|
+
: DEFAULTS.searchArgs
|
|
498
|
+
if (args.some(arg => typeof arg === 'string' && arg.endsWith('mcp_server.py'))) {
|
|
499
|
+
return { command, args: [...args, '--call', tool, payload] }
|
|
500
|
+
}
|
|
501
|
+
return { command, args: [...args, 'call', tool, payload] }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function nativeErrorText(stdout, stderr) {
|
|
505
|
+
try {
|
|
506
|
+
const parsed = JSON.parse(stdout)
|
|
507
|
+
const body = parsed?.content?.[0]?.text
|
|
508
|
+
if (typeof body === 'string' && body !== '') return body
|
|
509
|
+
} catch { /* ignore */ }
|
|
510
|
+
return stderr.trim() !== '' ? stderr : stdout
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function runNativeCall({ command, args, env }, timeoutMs, signal) {
|
|
514
|
+
return new Promise((resolve, reject) => {
|
|
515
|
+
if (signal && signal.aborted) {
|
|
516
|
+
reject(argoAborted())
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], env: env ?? process.env })
|
|
520
|
+
let stdout = ''
|
|
521
|
+
let stderr = ''
|
|
522
|
+
let settled = false
|
|
523
|
+
let timer = null
|
|
524
|
+
let detachAbort = null
|
|
525
|
+
|
|
526
|
+
const settle = (fn, value) => {
|
|
527
|
+
if (settled) return
|
|
528
|
+
settled = true
|
|
529
|
+
if (timer !== null) clearTimeout(timer)
|
|
530
|
+
if (detachAbort !== null) detachAbort()
|
|
531
|
+
fn(value)
|
|
532
|
+
}
|
|
533
|
+
const onAbort = () => {
|
|
534
|
+
try { child.kill('SIGKILL') } catch { /* already gone */ }
|
|
535
|
+
settle(reject, argoAborted())
|
|
536
|
+
}
|
|
537
|
+
if (signal) {
|
|
538
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
539
|
+
detachAbort = () => signal.removeEventListener('abort', onAbort)
|
|
540
|
+
}
|
|
541
|
+
timer = setTimeout(() => {
|
|
542
|
+
try { child.kill('SIGKILL') } catch { /* already gone */ }
|
|
543
|
+
settle(reject, new Error(`argo native call timed out after ${timeoutMs}ms`))
|
|
544
|
+
}, timeoutMs)
|
|
545
|
+
child.stdout.on('data', (chunk) => { stdout += chunk })
|
|
546
|
+
child.stderr.on('data', (chunk) => { stderr += chunk })
|
|
547
|
+
child.on('error', (err) => settle(reject, err))
|
|
548
|
+
child.on('close', (code) => {
|
|
549
|
+
if (code === 0) { settle(resolve, stdout); return }
|
|
550
|
+
settle(reject, new Error(`argo native call exited ${code}: ${clip(nativeErrorText(stdout, stderr), 500)}`))
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function resolveEntry(config) {
|
|
556
|
+
const command = typeof config.searchCommand === 'string' && config.searchCommand !== ''
|
|
557
|
+
? config.searchCommand
|
|
558
|
+
: DEFAULTS.searchCommand
|
|
559
|
+
const args = Array.isArray(config.searchArgs) && config.searchArgs.length > 0
|
|
560
|
+
? config.searchArgs
|
|
561
|
+
: DEFAULTS.searchArgs
|
|
562
|
+
return { command, args }
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function pickArgs(args, allowed) {
|
|
566
|
+
const out = {}
|
|
567
|
+
for (const [key, value] of Object.entries(args ?? {})) {
|
|
568
|
+
if (allowed.includes(key) && value !== undefined && value !== null && value !== '') {
|
|
569
|
+
out[key] = value
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return out
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export async function nativeViaMcpPool(config, kind, argsObj, timeoutMs, signal) {
|
|
576
|
+
const { command, args } = resolveEntry(config)
|
|
577
|
+
const conn = await acquireMcp({
|
|
578
|
+
command,
|
|
579
|
+
args,
|
|
580
|
+
idleMs: config.searchIdleMs ?? DEFAULTS.searchIdleMs,
|
|
581
|
+
max: config.searchConcurrency ?? DEFAULTS.searchConcurrency,
|
|
582
|
+
initMs: timeoutMs,
|
|
583
|
+
credentials: config.credentials,
|
|
584
|
+
config,
|
|
585
|
+
}, signal)
|
|
586
|
+
const result = await new Promise((resolve, reject) => {
|
|
587
|
+
const onAbort = () => reject(argoAborted())
|
|
588
|
+
if (signal !== undefined) {
|
|
589
|
+
if (signal.aborted) {
|
|
590
|
+
conn.release()
|
|
591
|
+
reject(argoAborted())
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
595
|
+
}
|
|
596
|
+
conn.request('tools/call', { name: kind, arguments: argsObj }, timeoutMs).then((value) => {
|
|
597
|
+
if (signal !== undefined) signal.removeEventListener('abort', onAbort)
|
|
598
|
+
resolve(value)
|
|
599
|
+
}, (err) => {
|
|
600
|
+
if (signal !== undefined) signal.removeEventListener('abort', onAbort)
|
|
601
|
+
reject(err)
|
|
602
|
+
})
|
|
603
|
+
})
|
|
604
|
+
if (result?.isError) {
|
|
605
|
+
const body = result?.content?.[0]?.text
|
|
606
|
+
throw argoToolError(typeof body === 'string' && body !== ''
|
|
607
|
+
? body
|
|
608
|
+
: `argo tool ${kind} returned an error`)
|
|
609
|
+
}
|
|
610
|
+
return JSON.stringify(result ?? {})
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export function makeNativeTool(config, kind, deps = {}) {
|
|
614
|
+
const spec = NATIVE_TOOL_SPECS[kind]
|
|
615
|
+
if (!spec) {
|
|
616
|
+
throw new Error(`argo-dsh: unknown native tool "${kind}" (known: ${Object.keys(NATIVE_TOOL_SPECS).join(', ')})`)
|
|
617
|
+
}
|
|
618
|
+
const spawnRun = deps.run ?? runNativeCall
|
|
619
|
+
const mcpCall = deps.mcpCall ?? nativeViaMcpPool
|
|
620
|
+
const forceSpawn = deps.run !== undefined && deps.mcpCall === undefined
|
|
621
|
+
|
|
622
|
+
const currentTimeout = () => {
|
|
623
|
+
const value = Number(config.nativeTimeoutMs)
|
|
624
|
+
return Number.isFinite(value) && value > 0 ? value : DEFAULTS.nativeTimeoutMs
|
|
625
|
+
}
|
|
626
|
+
const currentMaxChars = () => {
|
|
627
|
+
const value = Number(config.nativeMaxChars)
|
|
628
|
+
return Number.isFinite(value) && value > 0 ? value : DEFAULTS.nativeMaxChars
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
name: kind,
|
|
632
|
+
description: spec.description,
|
|
633
|
+
parameters: spec.parameters,
|
|
634
|
+
isConcurrencySafe: () => true,
|
|
635
|
+
output: {
|
|
636
|
+
schema: { type: 'object' },
|
|
637
|
+
render: (_args, value) => {
|
|
638
|
+
let body = ''
|
|
639
|
+
try {
|
|
640
|
+
const parsed = JSON.parse(value.stdout)
|
|
641
|
+
body = parsed?.content?.[0]?.text ?? value.stdout
|
|
642
|
+
} catch {
|
|
643
|
+
body = value.stdout
|
|
644
|
+
}
|
|
645
|
+
return text(clip(body, currentMaxChars()))
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
async execute(args, exec) {
|
|
649
|
+
const argsObj = pickArgs(args, spec.allowed)
|
|
650
|
+
const payload = JSON.stringify(argsObj)
|
|
651
|
+
const timeoutMs = currentTimeout()
|
|
652
|
+
const signal = exec?.signal
|
|
653
|
+
if (!forceSpawn) {
|
|
654
|
+
try {
|
|
655
|
+
return { stdout: await mcpCall(config, kind, argsObj, timeoutMs, signal) }
|
|
656
|
+
} catch (err) {
|
|
657
|
+
if (err?.code === 'ARGO_TOOL_ERROR' || err?.code === 'ARGO_TIMEOUT' || err?.code === 'WEB_ABORTED') throw err
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const spawnSpec = resolveNativeSpawn(config, kind, payload)
|
|
661
|
+
const env = await resolveEnv(undefined, config)
|
|
662
|
+
const stdout = await spawnRun(env !== undefined ? { ...spawnSpec, env } : spawnSpec, timeoutMs, signal)
|
|
663
|
+
return { stdout }
|
|
664
|
+
},
|
|
665
|
+
}
|
|
666
|
+
}
|