stillo
AIネイティブなターミナルブラウザ。Rustで実装。
stillo は distill(蒸留する)から作った造語です。Webページを蒸留して情報だけを抽出するというコンセプトを名前に込めています。
概要
stilloはターミナル上で動作するブラウザです。静的なHTMLページはそのまま取得し、React/Vue等のSPAはChrome CDP・Jina Reader・Firecrawl等へ自動委譲してコンテンツを取得します。取得したページはMarkdown/テキスト/JSONとして出力するか、TUIで対話的にブラウズできます。LLMと連携してQ&A・要約・構造化抽出を行う機能や、MCPサーバーとして他のAIエージェントに機能を提供する機能も備えています。
インストール
Nix (flake)
# ビルドして result/bin/stillo を使う
# ユーザープロファイルにインストール
# インストールせず直接実行
自分の nixpkgs に生やして home-manager / NixOS の設定に取り込む場合:
{
inputs.stillo.url = "github:stillo-browser/stillo";
# 方法1: overlay 経由(pkgs.stillo が使えるようになる)
nixpkgs.overlays = [ inputs.stillo.overlays.default ];
# home.packages = [ pkgs.stillo ];
# 方法2: package を直接渡す
# home.packages = [ inputs.stillo.packages.${pkgs.system}.default ];
}
ローカルのチェックアウトを参照するときは path:/path/to/stillo(git+file: でも可)。開発シェルは nix develop で入ります。
Cargo
# バイナリは target/release/stillo に生成されます
使い方
# TUIブラウザとして開く
# Markdownとして標準出力に出力
# 出力形式を指定
# SPA委譲先を明示
# JS委譲を無効化(静的HTMLのみ処理)
# タイムアウト秒数を指定(デフォルト30秒)
# 出力を切り詰める(長い一覧ページをLLMに渡すときなど)
# 詳細ログを出力(stderrに出力)
--format jsonはurl/title/content(Markdown本文)/content_chars/truncated/linksを返します。contentは--max-charsで切り詰められ、切り詰め時はtruncatedがtrueになります。- 標準出力が端末でない場合(パイプ・リダイレクト・CI)、
stillo <URL>とstillo search <query>は TUI を開かずに本文/検索結果をテキストで出力します。stillo search rust tokio | lessのようにそのまま使えます。 --format plainはタイトルのみを先頭に置き、Markdown の見出し記号は付けません。
サブコマンド
| コマンド | 説明 |
|---|---|
dump |
ページをMarkdown/テキスト/JSONとして stdout に出力 |
browse |
TUIブラウザとして起動 |
search |
Web検索して結果を表示(端末ではTUI、パイプ時はテキスト) |
qa |
ページについてLLMに質問(LLMプロバイダー設定が必要) |
summarize |
ページをLLMで要約(LLMプロバイダー設定が必要) |
extract |
指定フィールドをLLMで抽出(LLMプロバイダー設定が必要) |
mcp |
MCPサーバーとして起動(stdio transport、JSON-RPC 2.0) |
SPA委譲
SPAと判定されたページは以下の優先順位で委譲を試みます(いずれかが成功すれば以降はスキップ)。
- Chrome CDP —
localhost:9222でChromeが起動していれば使用 - Playwright Daemon — 自ユーザー所有のソケットが存在すれば使用(探索順:
STILLO_PLAYWRIGHT_SOCK→$XDG_RUNTIME_DIR/stillo-playwright.sock→/tmp/stillo-playwright.sock) - Jina Reader —
JINA_API_KEYが設定されているかSTILLO_ENABLE_JINA=1のときのみ使用(閲覧URLを外部サービスへ送信するためオプトイン) - Firecrawl —
FIRECRAWL_URLとFIRECRAWL_API_KEYが両方設定されていれば使用
委譲チェーン全体の所要時間は --timeout で打ち切られます。全ターゲットが失敗した場合は静的HTMLにフォールバックし、その旨を stderr に1行だけ案内します(JS rendering unavailable...)。
判定の挙動
- 本文が500文字以上のページは、フレームワークの痕跡があっても静的として処理します(SSR ページは委譲不要)。
- 本文が500文字未満のときだけ委譲を検討し、フレームワーク痕跡(
__NEXT_DATA__等)または JS 実行の痕跡(#root/#app/#svelteなどのマウント要素、512文字超のインラインスクリプト)がある場合に SPA と判定します。外部スクリプトの有無は根拠にしません(解析タグと区別できないため)。
Chrome CDP を使う場合
# --remote-debugging-port=9222 でChromeを起動しておく
Jina Reader を使う場合
# APIキーなしの無料枠を使う場合は STILLO_ENABLE_JINA=1
Firecrawl を使う場合
LLM連携
qa・summarize・extract サブコマンドはLLMにアクセスします。以下の環境変数で使用するプロバイダーを選択します(上から優先)。
| 優先度 | プロバイダー | 環境変数 |
|---|---|---|
| 1 | Anthropic | ANTHROPIC_API_KEY または ANTHROPIC_BASE_URL(モデル: ANTHROPIC_MODEL、デフォルト claude-sonnet-4-5)。ANTHROPIC_BASE_URL のみの場合は認証をプロキシ側で行う前提で、キーにはダミー値を送る |
| 2 | OpenAI | OPENAI_API_KEY(ベースURL: OPENAI_BASE_URL、モデル: OPENAI_MODEL、デフォルト gpt-4o-mini) |
| 3 | llama.cpp | LLAMA_CPP_BASE_URL(モデル: LLAMA_CPP_MODEL、デフォルト default、APIキー不要) |
| 4 | Ollama(ローカル) | OLLAMA_BASE_URL(例 http://localhost:11434/)、OLLAMA_MODEL(デフォルト llama3) |
いずれも設定されていない場合はエラーになります(Ollama への暗黙の接続は行いません)。ベースURLが不正な場合もエラーになります。OpenAI互換APIを持つ任意のプロバイダー(LM Studio等)は OPENAI_BASE_URL と OPENAI_API_KEY で利用できます。
ページ本文は <document> タグで区切ってLLMに渡し、本文中の指示に従わないよう system プロンプトで指定しています。出力が max_tokens に達して途中で切れた場合、qa・summarize は警告付きで部分出力を表示し、extract はエラーにします。
プロバイダーが1つも設定されておらず、既定の Ollama(localhost:11434)にも到達できない場合、qa / summarize / extract はページ取得の前に設定方法を示すエラーで終了します(無駄なフェッチをしません)。
# Anthropic でQ&A
# 要約
# フィールド抽出(カンマ区切りで複数指定)
# Ollama でローカル実行
MCPサーバー
stillo mcp でstdio transportのMCPサーバーとして起動します。Claude DesktopやClaude Codeなど、MCPクライアントからstilloの機能を呼び出せます。
利用可能なツール
| ツール | 説明 |
|---|---|
fetch_url |
URLを取得してMarkdown/テキスト/JSONで返す。max_chars(既定 50,000 文字)と offset でページング |
read_links |
URLからリンク一覧(テキスト・href)を抽出して返す。limit(既定 200 件)と offset でページング |
search_web |
Web検索して結果を返す(format=markdown|links)。ボットブロック時はエラーを明示 |
extract_structured |
LLMを使って指定フィールドをJSONで抽出する(LLM連携のプロバイダー設定が必要)。JSONとして解釈できない出力・途中で切れた出力はエラー |
MCP経由のページ取得は静的HTTP取得のみで、dump コマンドのようなSPA委譲やフレーム展開は行いません。切り詰めた場合は出力に全体長と次の offset を明記します。リクエストは並行処理され、notifications/cancelled で実行中のツール呼び出しを中断できます。
fetch_url・read_links・extract_structured は取得先URLをLLMが決めるため、loopback・プライベートアドレス(RFC1918)・リンクローカル(169.254.0.0/16 等)・ULA への接続を既定で拒否します。判定はDNS解決後のIPとリダイレクトの各ホップに対して行います。拒否モードでは HTTP_PROXY 等のプロキシ設定を使いません。ローカルの開発サーバー等を読ませる場合は STILLO_ALLOW_PRIVATE_NETWORK=1 を設定してください。URLスキームは http/https のみ受け付けます。CLI(stillo dump 等)にはこの制限はありません。
Web検索バックエンド
search_web と stillo search はバックエンドチェーンで検索します。ボットブロック(DDGの202チャレンジ等)を検出した場合は空結果ではなくエラーを返し、次のバックエンドへフォールバックします。
| 環境変数 | 効果 |
|---|---|
SEARXNG_URL |
SearXNG インスタンスを最優先バックエンドに追加(JSON API: /search?format=json) |
BRAVE_API_KEY |
Brave Search API(無料枠2000回/月)をバックエンドに追加 |
STILLO_SEARCH_BACKEND |
ddg|searxng|brave で単独バックエンド指定 |
DuckDuckGo HTML エンドポイントは常に末尾のフォールバックとして有効(ただし高頻度利用時はボットブロックされることがある)。
Claude Codeでの設定例
クレート構成
| クレート | 役割 |
|---|---|
stillo-core |
純粋関数のみ(HTML解析・コンテンツ抽出・Markdown変換) |
stillo-fetcher |
HTTP取得・SPA委譲 |
stillo-renderer |
TUI描画 |
stillo-llm |
LLM API呼び出し(Anthropic / OpenAI互換 / Ollama) |
stillo-mcp |
MCPサーバー(JSON-RPC 2.0 stdio transport) |
stillo (cli) |
コマンドライン引数解析・全体オーケストレーション |
将来構想
stilloネイティブモード
サーバとクライアントの主従を逆転させるコンセプト。従来のWebはサーバがHTMLでデザインごとコンテンツを送りつけるが、stilloネイティブモードではサーバはデータとセマンティクスのみを返し、表示・デザインはクライアントが全面的にコントロールする。
既存のWebへのフォールバックを維持しつつ、対応サーバにはよりリッチなセマンティクスを返させるオプトイン型の拡張として設計する想定。stillo mcp と組み合わせ、MCPサーバが変換層を担うことも視野に入れる。
開発
# ビルド
# テスト
# CDPサポートを有効にしてビルド
ログレベルは RUST_LOG 環境変数で制御できます(stderrに出力)。
RUST_LOG=debug
リリース手順
バージョン番号を上げて crates.io へ publish するには bump-version.sh を使います。
このスクリプトは以下を一括実行します。
Cargo.tomlのバージョンを書き換えcargo buildでビルド確認(Cargo.lockも更新)git commit(chore: bump version to X.Y.Z)git tag vX.Y.Zを作成git push origin main vX.Y.Z
タグ push をトリガーに GitHub Actions(.github/workflows/publish.yml)が自動で起動し、以下を行います。
cargo test --workspaceでテスト- 6クレートを依存順に crates.io へ publish
- GitHub Release を自動作成
事前準備
- リポジトリの Settings → Secrets →
CARGO_REGISTRY_TOKENに crates.io の API トークンを登録しておくこと - 全クレートのオーナーに登録済みのアカウントのトークンを使用すること(
cargo owner --add <user> <crate>で追加)