research-assistant 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README-ZH.md +115 -0
- package/README.md +124 -0
- package/npm/bin/research-assistant.js +70 -0
- package/npm/scripts/postinstall.js +112 -0
- package/package.json +38 -0
- package/pyproject.toml +84 -0
- package/src/research_assistant/__init__.py +7 -0
- package/src/research_assistant/__main__.py +6 -0
- package/src/research_assistant/assets/researcher_persona.md +94 -0
- package/src/research_assistant/assets/skills/research-assistant/SKILL.md +248 -0
- package/src/research_assistant/cli.py +155 -0
- package/src/research_assistant/commands/__init__.py +7 -0
- package/src/research_assistant/commands/ask.py +54 -0
- package/src/research_assistant/commands/doctor.py +201 -0
- package/src/research_assistant/commands/fetch.py +99 -0
- package/src/research_assistant/commands/locate.py +71 -0
- package/src/research_assistant/commands/search.py +161 -0
- package/src/research_assistant/commands/setup.py +249 -0
- package/src/research_assistant/commands/skills.py +60 -0
- package/src/research_assistant/config.py +258 -0
- package/src/research_assistant/errors.py +106 -0
- package/src/research_assistant/fetch/__init__.py +20 -0
- package/src/research_assistant/fetch/browser.py +613 -0
- package/src/research_assistant/fetch/cfbypass.py +318 -0
- package/src/research_assistant/fetch/html2md.py +44 -0
- package/src/research_assistant/fetch/normal.py +69 -0
- package/src/research_assistant/fetch/search_engine.py +324 -0
- package/src/research_assistant/fetch/snapshot.py +50 -0
- package/src/research_assistant/http.py +118 -0
- package/src/research_assistant/installer.py +270 -0
- package/src/research_assistant/locate/__init__.py +5 -0
- package/src/research_assistant/locate/engine.py +289 -0
- package/src/research_assistant/loginstate/__init__.py +10 -0
- package/src/research_assistant/loginstate/daemon.py +145 -0
- package/src/research_assistant/loginstate/daemon_cli.py +21 -0
- package/src/research_assistant/loginstate/daemonctl.py +116 -0
- package/src/research_assistant/loginstate/extension/background.js +167 -0
- package/src/research_assistant/loginstate/extension/manifest.json +15 -0
- package/src/research_assistant/loginstate/extension/popup.html +31 -0
- package/src/research_assistant/loginstate/extension/popup.js +33 -0
- package/src/research_assistant/output.py +140 -0
- package/src/research_assistant/providers/__init__.py +10 -0
- package/src/research_assistant/providers/base.py +93 -0
- package/src/research_assistant/providers/browser.py +91 -0
- package/src/research_assistant/providers/context7.py +171 -0
- package/src/research_assistant/providers/exa.py +353 -0
- package/src/research_assistant/providers/firecrawl.py +644 -0
- package/src/research_assistant/providers/openai_compat.py +293 -0
- package/src/research_assistant/providers/registry.py +62 -0
- package/src/research_assistant/providers/tavily.py +346 -0
- package/src/research_assistant/proxy.py +58 -0
- package/src/research_assistant/targets.py +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 research-assistant contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README-ZH.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# research-assistant
|
|
2
|
+
|
|
3
|
+
面向 AI agent 的并发 search / fetch / locate CLI 工具集。一次调用扇出到多个 provider 并行采集
|
|
4
|
+
候选信源和页面,然后由你自己阅读、得出答案。
|
|
5
|
+
|
|
6
|
+
阅读:[English](README.md)
|
|
7
|
+
|
|
8
|
+
> **中间件,不是神谕。** 它采集信源,不替你下结论。每次 search 返回候选 URL,每次 fetch 返回
|
|
9
|
+
> 页面正文;综合判断是你的事。
|
|
10
|
+
|
|
11
|
+
## 为什么需要
|
|
12
|
+
|
|
13
|
+
agent 常常耗费大量往返和上下文去做"一次并发工具集本来一次调用就能搞定"的事:跨引擎搜索、抓取
|
|
14
|
+
Cloudflare 后的页面、取官方文档、在长文件里定位段落。`research-assistant` 把这些采集工作收进
|
|
15
|
+
一个 CLI,并提供一个 `researcher` 子 agent 处理大型调研,保持主上下文干净。
|
|
16
|
+
|
|
17
|
+
## 特性
|
|
18
|
+
|
|
19
|
+
- **多源聚合搜索** — 一次 `search` 扇出到 `exa`、`tavily`、`firecrawl` 和无头浏览器引擎
|
|
20
|
+
(必应 / 谷歌),合并去重。**零配置可用**:浏览器源不需要 API key,永远在默认集合里。
|
|
21
|
+
- **Cloudflare / 反爬绕过** — `fetch` 驱动你**真实的** Edge/Chromium(有头,Cloudflare 检测不到
|
|
22
|
+
headless),点击穿过 Turnstile。自动升级:先走便宜的 API,失败才上真实浏览器。
|
|
23
|
+
- **一个浏览器,多个 tab** — 抓 N 个 URL 只开一个浏览器进程 + N 个 tab,不是 N 个浏览器。搜索
|
|
24
|
+
按首页结果数智能推断、并发翻页;跨进程实例上限防止浏览器失控。
|
|
25
|
+
- **登录 cookie 桥接** — 内置 MV3 扩展把日常浏览器的登录 cookie 注入 fetch,登录页无需重输凭据。
|
|
26
|
+
- **官方文档直取** — `ctx7 docs` 经 Context7 返回最新的库 / SDK / CLI / 云文档,比搜开放网页更快
|
|
27
|
+
更准。
|
|
28
|
+
- **web-LLM `ask` + 长文档 `locate`** — `ask` 用自带网页搜索的 LLM 回答自然语言问题并附引用;
|
|
29
|
+
`locate` 在阅读前锁定 10k 行文件里哪些段落重要。
|
|
30
|
+
- **插件式 provider** — 加一个 provider = 一个模块 + 一条配置;注册表自动发现,出现在 `--help`。
|
|
31
|
+
无需改路由或 CLI。
|
|
32
|
+
- **可脚本化** — stdout 默认 JSON(`--output markdown` 给人看);语义退出码(`0` 成功 · `1` 内部
|
|
33
|
+
· `2` 参数 · `3` 配置 · `4` 网络 · `5` 反爬)。可作为 skill 装进 Claude Code、Codex 等。
|
|
34
|
+
|
|
35
|
+
## 快速开始
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
uv venv .venv
|
|
39
|
+
uv pip install -e . --python .venv/Scripts/python.exe
|
|
40
|
+
research-assistant setup # 配置 provider + 安装 skill/agent(可选)
|
|
41
|
+
research-assistant search "python asyncio" --limit 10
|
|
42
|
+
research-assistant fetch https://example.com
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
没有 provider key?`search` 和 `fetch` 仍能用:浏览器源和 Firecrawl 的免 key 层不需要。
|
|
46
|
+
|
|
47
|
+
## 命令
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
# 搜索
|
|
51
|
+
research-assistant search "<q>" [--providers exa,tavily,firecrawl,browser] # 聚合
|
|
52
|
+
research-assistant browser search "<q>" [--engine bing-cn|bing-intl|google] # 直连浏览器引擎
|
|
53
|
+
research-assistant exa|tavily search "<q>" # 结构化 API
|
|
54
|
+
|
|
55
|
+
# 抓取
|
|
56
|
+
research-assistant fetch <url> [<url>...] # API,然后真实浏览器 + CF 绕过(自动)
|
|
57
|
+
research-assistant browser fetch <url>... # 直奔真实浏览器
|
|
58
|
+
research-assistant firecrawl scrape <url>... # 免 key markdown
|
|
59
|
+
|
|
60
|
+
# 文档 & 问答
|
|
61
|
+
research-assistant ctx7 docs /org/repo "<q>" # 官方文档(Context7)
|
|
62
|
+
research-assistant ask "<question>" # web-LLM 回答 + 引用
|
|
63
|
+
|
|
64
|
+
# 长文档
|
|
65
|
+
research-assistant locate <md_path> "<q>" # 锚点式相关性扫描
|
|
66
|
+
|
|
67
|
+
# 管理
|
|
68
|
+
research-assistant setup # 配置 + 安装 skill/agent
|
|
69
|
+
research-assistant doctor [--show-config] # 连通性诊断
|
|
70
|
+
research-assistant skills status # 受管 skill/agent 新鲜度
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
完整列表见 `research-assistant --help`;每条命令也支持 `-h`。
|
|
74
|
+
|
|
75
|
+
## 配置
|
|
76
|
+
|
|
77
|
+
配置在 `~/.research-assistant/config.toml`(环境变量 `RA_<TYPE>_<FIELD>` 覆盖):
|
|
78
|
+
|
|
79
|
+
```toml
|
|
80
|
+
schema_version = 1
|
|
81
|
+
|
|
82
|
+
[[provider]]
|
|
83
|
+
type = "exa"
|
|
84
|
+
api_key = "..."
|
|
85
|
+
base_url = "https://api.exa.ai"
|
|
86
|
+
|
|
87
|
+
[[provider]]
|
|
88
|
+
type = "openai_compat" # 驱动 `ask`
|
|
89
|
+
base_url = "https://api.example.com/v1"
|
|
90
|
+
api_key = "..."
|
|
91
|
+
model = "grok-..."
|
|
92
|
+
|
|
93
|
+
[proxy]
|
|
94
|
+
url = "" # 空 = 自动检测系统代理
|
|
95
|
+
|
|
96
|
+
[browser]
|
|
97
|
+
channel = "msedge" # msedge | chrome
|
|
98
|
+
max_browser_instances = 3 # 并发浏览器进程上限(跨 CLI)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
未配置的 provider 直接跳过。`firecrawl scrape` / `search` 和浏览器源是免 key 的,所以空配置下
|
|
102
|
+
`search` 和 `fetch` 也能跑。
|
|
103
|
+
|
|
104
|
+
## 工作原理
|
|
105
|
+
|
|
106
|
+
每条命令是对 provider 插件的一个薄 async 包装。`search` 扇出到各 provider 合并;`fetch` 先走便宜
|
|
107
|
+
的 API,失败升级到真实浏览器 + Cloudflare 绕过;`locate` 把文档切块、用小模型打分。输出默认 JSON
|
|
108
|
+
供主 agent 解析,另有 markdown 模式给人看。
|
|
109
|
+
|
|
110
|
+
大型调研可分派 `researcher` 子 agent:它在隔离上下文里跑完整的 search → fetch → locate 流程,把报告
|
|
111
|
+
写到磁盘,只返回摘要,重活阅读不污染主上下文。
|
|
112
|
+
|
|
113
|
+
## 许可
|
|
114
|
+
|
|
115
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# research-assistant
|
|
2
|
+
|
|
3
|
+
Concurrent search / fetch / locate CLI tools for AI agents. Fan out across many providers in
|
|
4
|
+
parallel, gather candidate sources and pages, then read them yourself to form the answer.
|
|
5
|
+
|
|
6
|
+
Read this in: [中文](README-ZH.md)
|
|
7
|
+
|
|
8
|
+
> **Middleware, not an oracle.** It collects sources. It does not write your conclusion. Every
|
|
9
|
+
> search returns candidate URLs, every fetch returns the page body; synthesis is your job.
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
Agents burn turns and context doing what one concurrent tool-set can do in a single call: search
|
|
14
|
+
across engines, fetch pages past Cloudflare, pull official docs, locate passages in long files.
|
|
15
|
+
`research-assistant` does all of that gathering in one CLI, and ships a `researcher` sub-agent
|
|
16
|
+
for large investigations that keeps the host context clean.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Aggregated multi-source search** — one `search` call fans out across `exa`, `tavily`,
|
|
21
|
+
`firecrawl`, and a headless browser engine (Bing/Google), then merges and de-duplicates.
|
|
22
|
+
**Zero-config works**: the browser source needs no API key and is always in the default set.
|
|
23
|
+
- **Cloudflare & anti-bot bypass** — `fetch` drives your **real** Edge/Chromium (headed, so
|
|
24
|
+
Cloudflare does not flag it) and clicks through Turnstile. It auto-escalates: cheap API first,
|
|
25
|
+
real browser only when the API fails.
|
|
26
|
+
- **One browser, many tabs** — fetching N URLs opens one browser process with N tabs, not N
|
|
27
|
+
browsers. Search paginates concurrently with smart page-count inference; a cross-process
|
|
28
|
+
instance cap prevents runaway browsers.
|
|
29
|
+
- **Login cookies bridged** — a bundled MV3 extension pipes your daily browser's login cookies
|
|
30
|
+
into fetch, so logged-in pages work without re-entering credentials.
|
|
31
|
+
- **Official docs from the source** — `ctx7 docs` returns up-to-date library / SDK / CLI / cloud
|
|
32
|
+
docs via Context7, faster and more accurate than grepping the open web.
|
|
33
|
+
- **Web-LLM `ask` and long-doc `locate`** — `ask` gets a natural-language answer with citations
|
|
34
|
+
from a web-enabled LLM; `locate` pins which sections of a 10k-line file matter before you read.
|
|
35
|
+
- **Plugin providers** — adding a provider is one module plus one config entry; the registry
|
|
36
|
+
auto-discovers it and it appears in `--help`. No router or CLI changes.
|
|
37
|
+
- **Scriptable** — JSON on stdout by default (`--output markdown` for humans); semantic exit
|
|
38
|
+
codes (`0` ok · `1` internal · `2` args · `3` config · `4` network · `5` antibot). Installs as
|
|
39
|
+
a skill into Claude Code, Codex, and friends.
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
uv venv .venv
|
|
45
|
+
uv pip install -e . --python .venv/Scripts/python.exe
|
|
46
|
+
research-assistant setup # configure providers + install skill/agent (optional)
|
|
47
|
+
research-assistant search "python asyncio" --limit 10
|
|
48
|
+
research-assistant fetch https://example.com
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
No provider keys? `search` and `fetch` still work: the browser source and Firecrawl's keyless
|
|
52
|
+
tier need none.
|
|
53
|
+
|
|
54
|
+
## Commands
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
# Search
|
|
58
|
+
research-assistant search "<q>" [--providers exa,tavily,firecrawl,browser] # aggregated
|
|
59
|
+
research-assistant browser search "<q>" [--engine bing-cn|bing-intl|google] # direct browser engine
|
|
60
|
+
research-assistant exa|tavily search "<q>" # structured APIs
|
|
61
|
+
|
|
62
|
+
# Fetch
|
|
63
|
+
research-assistant fetch <url> [<url>...] # API, then real browser + CF bypass (auto)
|
|
64
|
+
research-assistant browser fetch <url>... # straight to the real browser
|
|
65
|
+
research-assistant firecrawl scrape <url>... # keyless markdown
|
|
66
|
+
|
|
67
|
+
# Docs & Q&A
|
|
68
|
+
research-assistant ctx7 docs /org/repo "<q>" # official docs via Context7
|
|
69
|
+
research-assistant ask "<question>" # web-LLM answer + citations
|
|
70
|
+
|
|
71
|
+
# Long docs
|
|
72
|
+
research-assistant locate <md_path> "<q>" # anchor-based relevance scan
|
|
73
|
+
|
|
74
|
+
# Management
|
|
75
|
+
research-assistant setup # configure + install skill/agent
|
|
76
|
+
research-assistant doctor [--show-config] # connectivity diagnostics
|
|
77
|
+
research-assistant skills status # managed skill/agent freshness
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Run `research-assistant --help` for the full list; every command also takes `-h`.
|
|
81
|
+
|
|
82
|
+
## Configuration
|
|
83
|
+
|
|
84
|
+
Config lives at `~/.research-assistant/config.toml` (env overrides via `RA_<TYPE>_<FIELD>`):
|
|
85
|
+
|
|
86
|
+
```toml
|
|
87
|
+
schema_version = 1
|
|
88
|
+
|
|
89
|
+
[[provider]]
|
|
90
|
+
type = "exa"
|
|
91
|
+
api_key = "..."
|
|
92
|
+
base_url = "https://api.exa.ai"
|
|
93
|
+
|
|
94
|
+
[[provider]]
|
|
95
|
+
type = "openai_compat" # drives `ask`
|
|
96
|
+
base_url = "https://api.example.com/v1"
|
|
97
|
+
api_key = "..."
|
|
98
|
+
model = "grok-..."
|
|
99
|
+
|
|
100
|
+
[proxy]
|
|
101
|
+
url = "" # empty = auto-detect system proxy
|
|
102
|
+
|
|
103
|
+
[browser]
|
|
104
|
+
channel = "msedge" # msedge | chrome
|
|
105
|
+
max_browser_instances = 3 # concurrent browser-process cap (cross-CLI)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Unconfigured providers are simply skipped. `firecrawl scrape` / `search` and the browser source
|
|
109
|
+
are keyless, so `search` and `fetch` work with an empty config.
|
|
110
|
+
|
|
111
|
+
## How it works
|
|
112
|
+
|
|
113
|
+
Each command is a thin async wrapper over a provider plugin. `search` fans out across providers
|
|
114
|
+
and merges; `fetch` tries the cheap API then escalates to a real browser with Cloudflare bypass;
|
|
115
|
+
`locate` chunks a document and scores chunks with a small model. Output is JSON by default so the
|
|
116
|
+
host agent can parse it, with a markdown mode for humans.
|
|
117
|
+
|
|
118
|
+
For large investigations, dispatch the `researcher` sub-agent: it runs the full
|
|
119
|
+
search → fetch → locate workflow in isolation, writes a report to disk, and returns only a
|
|
120
|
+
summary, so the heavy reading never pollutes the host context.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin shim:找到 venv python 并转发到 `python -m research_assistant.cli <args>`。
|
|
3
|
+
// venv 缺失时自动修复(跑一次 postinstall)。
|
|
4
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
|
|
8
|
+
const packageRoot = path.resolve(__dirname, "..", "..");
|
|
9
|
+
const callerCwd = process.env.INIT_CWD || process.cwd();
|
|
10
|
+
const venvDir = path.join(packageRoot, ".research-assistant-python");
|
|
11
|
+
const pythonPath =
|
|
12
|
+
process.platform === "win32"
|
|
13
|
+
? path.join(venvDir, "Scripts", "python.exe")
|
|
14
|
+
: path.join(venvDir, "bin", "python");
|
|
15
|
+
|
|
16
|
+
function reinstallHint() {
|
|
17
|
+
console.error("修复:重装 npm 包");
|
|
18
|
+
console.error(" npm install -g research-assistant");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(pythonPath)) {
|
|
22
|
+
const postinstall = path.join(packageRoot, "npm", "scripts", "postinstall.js");
|
|
23
|
+
console.error("research-assistant Python 运行时缺失,尝试自动修复...");
|
|
24
|
+
const repaired = spawnSync(process.execPath, [postinstall], {
|
|
25
|
+
cwd: packageRoot,
|
|
26
|
+
stdio: "inherit",
|
|
27
|
+
windowsHide: true,
|
|
28
|
+
env: { ...process.env, PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" },
|
|
29
|
+
});
|
|
30
|
+
if (repaired.error) {
|
|
31
|
+
console.error(`运行时修复失败:${repaired.error.message}`);
|
|
32
|
+
reinstallHint();
|
|
33
|
+
process.exit(5);
|
|
34
|
+
}
|
|
35
|
+
if (repaired.status !== 0 || !fs.existsSync(pythonPath)) {
|
|
36
|
+
console.error("research-assistant 找不到 Python 运行时。");
|
|
37
|
+
console.error(`期望位置:${pythonPath}`);
|
|
38
|
+
reinstallHint();
|
|
39
|
+
process.exit(repaired.status || 5);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const child = spawn(
|
|
44
|
+
pythonPath,
|
|
45
|
+
["-m", "research_assistant.cli", ...process.argv.slice(2)],
|
|
46
|
+
{
|
|
47
|
+
cwd: callerCwd,
|
|
48
|
+
stdio: "inherit",
|
|
49
|
+
env: {
|
|
50
|
+
...process.env,
|
|
51
|
+
RESEARCH_ASSISTANT_PACKAGE_ROOT: packageRoot,
|
|
52
|
+
PYTHONIOENCODING: process.env.PYTHONIOENCODING || "utf-8",
|
|
53
|
+
PYTHONUTF8: process.env.PYTHONUTF8 || "1",
|
|
54
|
+
},
|
|
55
|
+
windowsHide: true,
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
child.on("error", (error) => {
|
|
60
|
+
console.error(`启动 research-assistant 失败:${error.message}`);
|
|
61
|
+
process.exit(5);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
child.on("close", (code, signal) => {
|
|
65
|
+
if (signal) {
|
|
66
|
+
process.kill(process.pid, signal);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
process.exit(code ?? 5);
|
|
70
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// postinstall: 建 venv + 安装 Python 包(ADR-0003:uv 优先,回退 python -m venv + pip)。
|
|
2
|
+
// PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 —— 用本地浏览器 channel,不下载 Chromium。
|
|
3
|
+
const { spawnSync } = require("node:child_process");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
|
|
7
|
+
const packageRoot = path.resolve(__dirname, "..", "..");
|
|
8
|
+
const venvDir = path.join(packageRoot, ".research-assistant-python");
|
|
9
|
+
|
|
10
|
+
function run(command, args, options = {}) {
|
|
11
|
+
const env = {
|
|
12
|
+
...process.env,
|
|
13
|
+
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1",
|
|
14
|
+
...(options.env || {}),
|
|
15
|
+
};
|
|
16
|
+
const result = spawnSync(command, args, {
|
|
17
|
+
cwd: packageRoot,
|
|
18
|
+
stdio: options.stdio || "inherit",
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
env,
|
|
21
|
+
windowsHide: true,
|
|
22
|
+
});
|
|
23
|
+
if (result.error) {
|
|
24
|
+
return { ok: false, error: result.error };
|
|
25
|
+
}
|
|
26
|
+
return { ok: result.status === 0, status: result.status, stdout: result.stdout || "" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function venvPython() {
|
|
30
|
+
return process.platform === "win32"
|
|
31
|
+
? path.join(venvDir, "Scripts", "python.exe")
|
|
32
|
+
: path.join(venvDir, "bin", "python");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hasCommand(cmd) {
|
|
36
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
37
|
+
const r = run(finder, [cmd], { stdio: "pipe" });
|
|
38
|
+
return r.ok;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pythonCandidates() {
|
|
42
|
+
if (process.platform === "win32") {
|
|
43
|
+
return [
|
|
44
|
+
{ command: "py", args: ["-3"] },
|
|
45
|
+
{ command: "python", args: [] },
|
|
46
|
+
{ command: "python3", args: [] },
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
return [
|
|
50
|
+
{ command: "python3", args: [] },
|
|
51
|
+
{ command: "python", args: [] },
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function findPython() {
|
|
56
|
+
const probe = ["-c", "import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)"];
|
|
57
|
+
for (const c of pythonCandidates()) {
|
|
58
|
+
const r = run(c.command, [...c.args, ...probe], { stdio: "pipe" });
|
|
59
|
+
if (r.ok) return c;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---- 主流程 ----
|
|
65
|
+
let useUv = hasCommand("uv");
|
|
66
|
+
|
|
67
|
+
// 1) 建 venv
|
|
68
|
+
if (!fs.existsSync(venvPython())) {
|
|
69
|
+
console.log("Creating research-assistant Python runtime...");
|
|
70
|
+
if (useUv) {
|
|
71
|
+
const created = run("uv", ["venv", venvDir, "--python", "3.10"]);
|
|
72
|
+
if (!created.ok) {
|
|
73
|
+
console.error("uv venv 创建失败,回退到 python -m venv");
|
|
74
|
+
useUv = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!fs.existsSync(venvPython())) {
|
|
78
|
+
const py = findPython();
|
|
79
|
+
if (!py) {
|
|
80
|
+
console.error("research-assistant 需要 Python 3.10+。请安装 Python 后重试:");
|
|
81
|
+
console.error(" npm install -g research-assistant");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
// python -m venv 自带 pip;uv venv 不带(uv 自己管包),故回退时重建带 pip 的 venv
|
|
85
|
+
const created = run(py.command, [...py.args, "-m", "venv", "--clear", venvDir]);
|
|
86
|
+
if (!created.ok) {
|
|
87
|
+
console.error("无法创建 Python 虚拟环境。");
|
|
88
|
+
process.exit(created.status || 1);
|
|
89
|
+
}
|
|
90
|
+
useUv = false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2) 安装包(uv venv 用 uv pip;python venv 用 pip)
|
|
95
|
+
console.log("Installing research-assistant Python package" + (useUv ? " (via uv)" : "") + "...");
|
|
96
|
+
let installOk = false;
|
|
97
|
+
if (useUv) {
|
|
98
|
+
const r = run("uv", ["pip", "install", "--python", venvPython(), packageRoot]);
|
|
99
|
+
installOk = r.ok;
|
|
100
|
+
}
|
|
101
|
+
if (!installOk) {
|
|
102
|
+
// 非 uv 路径(python -m venv 自带 pip)
|
|
103
|
+
const r = run(venvPython(), [
|
|
104
|
+
"-m", "pip", "install", "--disable-pip-version-check", packageRoot,
|
|
105
|
+
]);
|
|
106
|
+
if (!r.ok) {
|
|
107
|
+
console.error("安装 research-assistant Python 包失败。");
|
|
108
|
+
process.exit(r.status || 1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log("research-assistant ready.");
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "research-assistant",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Search/research sub-agent + concurrent search/fetch/locate CLI atomic tools for AI agents (Exa, Tavily, Firecrawl, Context7, OpenAI-compatible).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/arsonist-g/research-assistant-cli#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/arsonist-g/research-assistant-cli.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/arsonist-g/research-assistant-cli/issues"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"research-assistant": "npm/bin/research-assistant.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"npm/",
|
|
19
|
+
"src/research_assistant/**/*.py",
|
|
20
|
+
"src/research_assistant/assets/researcher_persona.md",
|
|
21
|
+
"src/research_assistant/assets/skills/research-assistant/SKILL.md",
|
|
22
|
+
"src/research_assistant/loginstate/extension/**",
|
|
23
|
+
"pyproject.toml",
|
|
24
|
+
"README.md",
|
|
25
|
+
"README-ZH.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"postinstall": "node npm/scripts/postinstall.js"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"registry": "https://registry.npmjs.org"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/pyproject.toml
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "research-assistant"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Search/research sub-agent + a set of concurrent search/fetch/locate CLI atomic tools for AI agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "research-assistant" }]
|
|
13
|
+
keywords = ["search", "research", "cli", "agent"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
# HTTP 客户端(各 provider 自写 HTTP,ADR-0010)
|
|
24
|
+
"httpx>=0.27",
|
|
25
|
+
# DrissionPage:CF 绕过兜底路径。直驱用户 Edge(不经自动化运行时,无注入痕迹)+
|
|
26
|
+
# DOM.getDocument(pierce) 穿透 closed shadow root 点 Turnstile checkbox(playwright 的
|
|
27
|
+
# performSearch 只穿透 UA shadow DOM,看不到 closed shadow root 里的 checkbox)。同步 API,
|
|
28
|
+
# browser.py 用 asyncio.to_thread 桥接到 async。
|
|
29
|
+
"DrissionPage>=4.1",
|
|
30
|
+
# pywin32:Windows 下隐藏 CF 兜底浏览器窗口(page.set.hide → Win32 ShowWindow SW_HIDE)。
|
|
31
|
+
# 窗口仍在(真 headed 渲染,CF 检测不到 headless),但不占视野。仅 Windows 需要。
|
|
32
|
+
"pywin32; sys_platform == 'win32'",
|
|
33
|
+
# HTML → markdown(浏览器回退路径用,ADR-0006 D6)
|
|
34
|
+
"trafilatura>=1.12",
|
|
35
|
+
# TOML 写入(setup 持久化配置,D3)
|
|
36
|
+
"tomli_w>=1.0",
|
|
37
|
+
# cookie daemon:单 aiohttp 进程同时承载 WS(扩展连)+ HTTP(fetch/doctor 调),ADR-0006
|
|
38
|
+
"aiohttp>=3.9",
|
|
39
|
+
# Python 3.10 没有 tomllib,需 tomli 读 TOML;3.11+ 内置 tomllib(环境标记自动按版本装)
|
|
40
|
+
"tomli>=2.0; python_version < '3.11'",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
# 测试依赖(开发期)。CI/贡献者:`uv pip install -e ".[dev]"` 安装。
|
|
45
|
+
dev = [
|
|
46
|
+
"pytest>=8.0",
|
|
47
|
+
"pytest-asyncio>=0.23.0",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.scripts]
|
|
51
|
+
research-assistant = "research_assistant.cli:main"
|
|
52
|
+
|
|
53
|
+
[project.urls]
|
|
54
|
+
Homepage = "https://github.com/arsonist-g/research-assistant"
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build]
|
|
57
|
+
# 中文审查版(SKILL-ZH.md / researcher_persona-ZH.md)仅供人工审阅:
|
|
58
|
+
# 不进 git、不进发行包。installer 固定读 SKILL.md / researcher_persona.md,本就不会安装。
|
|
59
|
+
exclude = [
|
|
60
|
+
"src/research_assistant/assets/skills/research-assistant/SKILL-ZH.md",
|
|
61
|
+
"src/research_assistant/assets/researcher_persona-ZH.md",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.hatch.build.targets.wheel]
|
|
65
|
+
packages = ["src/research_assistant"]
|
|
66
|
+
# assets/(SKILL.md、researcher 人设)与 loginstate/extension/(MV3 扩展)在包目录下,
|
|
67
|
+
# 随 packages 递归包含,无需 force-include。
|
|
68
|
+
|
|
69
|
+
[tool.hatch.build.targets.sdist]
|
|
70
|
+
include = [
|
|
71
|
+
"src",
|
|
72
|
+
"README.md",
|
|
73
|
+
"LICENSE",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.pytest.ini_options]
|
|
77
|
+
testpaths = ["tests"]
|
|
78
|
+
asyncio_mode = "auto"
|
|
79
|
+
markers = [
|
|
80
|
+
"live_cf: 真实 Cloudflare 绕过实机测试(启动浏览器、访问外部站点,慢且依赖网络;默认跳过,手动 `pytest -m live_cf` 运行)",
|
|
81
|
+
"live_browser: browser 平台 live 测试(启动浏览器、访问外部站点;默认跳过,手动 `pytest -m live_browser` 运行)",
|
|
82
|
+
]
|
|
83
|
+
# 默认排除实机测试,保持快速基线干净;命令行 `-m live_cf` / `-m live_browser` 会覆盖此项
|
|
84
|
+
addopts = "-m 'not live_cf and not live_browser'"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# researcher
|
|
2
|
+
|
|
3
|
+
You are a **research sub-agent**. A host agent delegates a question to you that needs external
|
|
4
|
+
information. You run in an **isolated context**: the host sees only what you return, so your
|
|
5
|
+
job is to do the heavy reading here and hand back a trustworthy, source-grounded answer.
|
|
6
|
+
|
|
7
|
+
**All external-information gathering MUST go through the `research-assistant` skill.** It is
|
|
8
|
+
your toolkit and operating manual: the full tool map (`fetch` with automatic Cloudflare-bypass
|
|
9
|
+
escalation; `search` aggregating the search APIs and the headless browser engine; Context7
|
|
10
|
+
official docs; the `ask` web-LLM; `locate` for long docs), the concurrency strategy, and every
|
|
11
|
+
command with its strength and "use when". Pick tools per the skill's guidance. Do not improvise
|
|
12
|
+
sources or guess at tool behavior.
|
|
13
|
+
|
|
14
|
+
## 0. Why you exist, and the one rule that matters most
|
|
15
|
+
|
|
16
|
+
The host delegates to you precisely because gathering and reading sources is **token-expensive
|
|
17
|
+
and context-polluting**. Doing that work in the host's context would crowd out the actual
|
|
18
|
+
task. That is why you run isolated.
|
|
19
|
+
|
|
20
|
+
The corollary is the most important rule in this document:
|
|
21
|
+
|
|
22
|
+
> **Do not over-economize on tokens.** You are isolated; reading thoroughly here costs the
|
|
23
|
+
> host nothing. The failure mode to avoid is *skimming and guessing*: reading only a snippet,
|
|
24
|
+
> inferring the rest, and returning a plausible-sounding but unverified answer. Reading the
|
|
25
|
+
> full relevant source is always cheaper than being wrong. When in doubt between "read more"
|
|
26
|
+
> and "move on", read more.
|
|
27
|
+
|
|
28
|
+
## 1. Consume sources, do not inherit synthesis
|
|
29
|
+
|
|
30
|
+
Every search tool returns **candidate sources** (URLs, snippets, citations), not conclusions
|
|
31
|
+
to trust verbatim.
|
|
32
|
+
|
|
33
|
+
- `search` returns **candidate URLs with snippets**, aggregated from the search APIs (exa,
|
|
34
|
+
tavily) and the headless browser search engine. The snippets come from the source pages,
|
|
35
|
+
not an LLM, so they are usable as leads; still `fetch` and read the real page before you
|
|
36
|
+
rely on any detail, because a snippet is an excerpt, not the whole story.
|
|
37
|
+
- `ask` (the web-enabled LLM) is **breadth only, low trust**. It is good for discovering what
|
|
38
|
+
sources exist on a fuzzy or wide topic. It is unreliable for any specific fact: it
|
|
39
|
+
hallucinates URLs, mangles details, and gets confused by context pollution. Never copy its
|
|
40
|
+
prose into your report. Use it to find source URLs, then `fetch` and read the real pages.
|
|
41
|
+
- For any actual claim, fetch the cited page and **read it yourself**. Your conclusion is
|
|
42
|
+
what you read, not what a search snippet or an LLM said.
|
|
43
|
+
- Never fabricate a source you did not fetch. If you cannot find a trustworthy source for a
|
|
44
|
+
claim, say so (see §3).
|
|
45
|
+
|
|
46
|
+
## 2. Heuristic read strategy: pre-filter, then read fully, but never trust the filter blindly
|
|
47
|
+
|
|
48
|
+
You will often face more sources than are worth reading cover-to-cover. Use a two-stage
|
|
49
|
+
strategy:
|
|
50
|
+
|
|
51
|
+
1. **Pre-filter to narrow the field.** Use the cheap, broad operations the skill provides:
|
|
52
|
+
`search` for candidate URLs and snippets (it aggregates the search APIs and the headless
|
|
53
|
+
browser engine; the browser source is always included, so it works with zero config and
|
|
54
|
+
reaches niche or anti-bot-protected sources the APIs miss), `ctx7 docs` for
|
|
55
|
+
library/framework questions, `locate` to pin relevant passages in a long fetched page, or
|
|
56
|
+
a small/fast model pass over titles and snippets to rank which URLs deserve a full fetch.
|
|
57
|
+
2. **Then read the top candidates in full.** The pre-filter is a *hint*, not a verdict. Fetch
|
|
58
|
+
and read the most promising sources completely. `fetch` climbs automatically (API, then a
|
|
59
|
+
real browser with Cloudflare bypass and login cookies), so pages behind anti-bot or a
|
|
60
|
+
login still come back. The actual answer usually lives in the body, not the snippet.
|
|
61
|
+
|
|
62
|
+
**Do not 100% trust the pre-filter.** This is the second most important rule. Small-model
|
|
63
|
+
relevance scores and snippet matches are noisy: they can rank a crucial source low, or miss
|
|
64
|
+
it entirely. When your confidence in the ranking is low (the topic is subtle, the snippets
|
|
65
|
+
are ambiguous, the question has multiple valid framings), **widen the net**: fetch and read
|
|
66
|
+
more sources fully rather than trusting a skim. It is better to read three extra pages than
|
|
67
|
+
to miss the one that holds the answer. Cheap pre-filtering serves thorough reading; it must
|
|
68
|
+
never *replace* it.
|
|
69
|
+
|
|
70
|
+
## 3. Honesty over confidence
|
|
71
|
+
|
|
72
|
+
- Distinguish what you **verified** (you fetched and read a trustworthy source) from what you
|
|
73
|
+
**inferred**. Never present an unverified inference as a sourced fact.
|
|
74
|
+
- If no trustworthy source is found, or a claim could not be verified, say so explicitly. "I
|
|
75
|
+
could not verify X" is a correct and complete answer; a confident-sounding fabrication is
|
|
76
|
+
not.
|
|
77
|
+
|
|
78
|
+
## 4. Output contract
|
|
79
|
+
|
|
80
|
+
- Write the **full report** to `tmp-doc/research-<topic>-<timestamp>.md`. Frontmatter: `task`,
|
|
81
|
+
`created_at`, `sources`. Write it complete enough that the host, or any later reader, can
|
|
82
|
+
rely on this report alone without going back to the original sources.
|
|
83
|
+
- **Return to the caller** (this is all the host sees): a concise summary (a few sentences),
|
|
84
|
+
the key conclusions with their supporting sources, and the source URL list. Do not dump raw
|
|
85
|
+
page bodies back. Keep the host's context clean; the bodies live in the on-disk report.
|
|
86
|
+
|
|
87
|
+
## Summary of principles
|
|
88
|
+
|
|
89
|
+
1. All gathering goes through the `research-assistant` skill; do not improvise sources.
|
|
90
|
+
2. Read thoroughly; isolation makes reading cheap, and skimming is the failure mode.
|
|
91
|
+
3. Consume sources, do not inherit synthesis (especially LLM web-search prose).
|
|
92
|
+
4. Pre-filter to narrow, then read fully, but never blindly trust the pre-filter.
|
|
93
|
+
5. Honesty over confidence: separate verified from inferred; say what you do not know.
|
|
94
|
+
6. Write the full report to disk; return a tight, source-grounded summary.
|