[Python]Hono CLIを参考にCLI型コーディングエージェントでドキュメント参照できるようにした(Click)
はじめに
最近XでこのPostを見ました。
なるほど。
やっていきます。
今回実装するCLI型コーディングエージェントShaftはこちら。
Hono CLI Docs
Hono CLI v0.1.0のDocsコマンドはどうなっているのでしょうか?
ソースコードを確認してみましょう。
非常にシンプルな仕組みなのですぐ理解できましたが、解説が面倒なのでShaftに解説してもらいました。
$ shaft ask "Explanation must be in Japanese" -f index.ts
Persona: programmer
Model: gemini-flash-latest
Executing phase 1/1 using programmer.json
Processing files:
- C:\Users\shaft\Desktop\index.ts
Explanation for C:\Users\shaft\Desktop\index.ts:
## `docsCommand` におけるドキュメント取得ロジック
`shaft/index.ts`ファイル内の`docsCommand`関数は、Honoのドキュメントを表示するためのCLIコマンドを定義しています。このコマンドの主要な機能は、ユーザーが指定したパスに基づいて、関連するMarkdownファイルをリモートから取得し、コンソールに出力することです。
### 1. パスの決定優先順位
ドキュメントパス(`finalPath`)は、以下の優先順位で決定されます。
1. **CLI引数**: コマンドラインで直接指定されたパス(例: `hono docs /docs/concepts/motivation`)。
2. **標準入力 (stdin) のパイプ**: CLI引数が提供されない場合、`process.stdin.isTTY`でパイプされているか確認します。パイプ入力(例: `echo "examples/stytch-auth" | hono docs`)があれば、それをパスとして使用します。この際、`jq`などの出力に対応するため、入力から引用符が削除されます。
3. **デフォルト**: 上記のいずれでもパスが確定しなかった場合、特別なケースとして`https://hono.dev/llms.txt`を取得・表示し、コマンドを終了します。
### 2. URLの構築とフェッチ
パスが決定された後、ファイルを取得するために2つのURLが構築されます。
1. **`markdownUrl`**: ドキュメントソースのMarkdownファイルを直接指すGitHub Raw URLです。これはプライマリな取得先となります。(例: `https://raw.githubusercontent.com/honojs/website/.../path.md`)
2. **`webUrl`**: 取得に失敗した場合のフォールバックとしてユーザーに提示されるHonoの公式サイト上のWeb URLです。(例: `https://hono.dev/path`)
取得処理は非同期関数`fetchAndDisplayContent`によって実行されます。この関数は`markdownUrl`へのフェッチを試み、成功すればその内容を出力します。ネットワークエラーやHTTPステータスが`ok`でない場合(例: 404 Not Found)、エラーメッセージと共にフォールバックURL (`webUrl`) を表示するよう設計されています。
Shaft Output Docs
では同じようにHTTPXライブラリあたりのHTTPリクエストクライアントを使い、URLから取得するshaft output docsコマンドを実装していこうと思いましたが、はたしてそれでいいのでしょうか?
せっかくShaftに実装するので、Shaftのドキュメントにあわせた設計にしたいです。
ShaftはHonoと異なり、CLIツールと同じリポジトリでドキュメントを管理しているため、ツールインストール時に全てのdocsディレクトリファイルを取得できるようにすれば、HTTPXライブラリを使わずにファイルを読み込みオフラインで表示可能なことに気づきました。
また、Honoは公式ドキュメントが英語のみですが、Shaftは英語と日本語の多言語ドキュメントを用意しているため、こちらもルーティングできるようにします。
というわけでShaft v2.7.0で実装しました。
@output.command("docs")
@click.argument("pattern", required=False)
def output_docs(pattern: Optional[str]) -> None:
"""Display content of documentation files. You can filter the output by providing a pattern (e.g., `en/*`)."""
handle_docs(pattern)
def handle_docs(pattern: Optional[str]) -> None:
"""Display content of documents. Optionally filter by pattern."""
docs_dir = absolute_path(os.path.join(os.path.dirname(__file__), "..", "docs"))
results = []
if pattern is None:
pattern = "index.md"
norm_pattern = os.path.normcase(pattern)
for root, _, files in os.walk(docs_dir):
for file in files:
full_path = os.path.join(root, file)
rel_path = os.path.relpath(full_path, docs_dir)
if isinstance(rel_path, bytes):
rel_path = os.fsdecode(rel_path)
norm_path = os.path.normcase(rel_path)
if matches_pattern(norm_path, norm_pattern):
results.append(rel_path)
output_parts = []
for result_path in sorted(results):
full_path = os.path.join(docs_dir, result_path)
if full_path.endswith((".md", ".json")):
try:
content = read_file_content(full_path)
output_parts.append(content)
except (FileNotFoundError, IOError) as e:
click.echo(click.style(f"Warning: Could not read file {full_path}: {e}", fg="yellow"), err=True)
final_output = "\n".join(output_parts)
click.echo(final_output, nl=False)
shaft output docsコマンドでindex.mdファイルを表示できます(shaft output docs index.mdと等価)
$ shaft output docs
# Shaft
A flexible AI coding agent for the CLI, powered by a customizable prompt engine.
## Docs
- English: [Getting Started](en/getting_started.md), [User Guide](en/user_guide.md), [Persona](en/persona.md)
- 日本語: [はじめに](ja/getting_started.md), [ユーザーガイド](ja/user_guide.md), [ペルソナ](ja/persona.md)
英語のGetting Startedを表示するにはこう。
$ shaft output docs en/getting_started.md
JSONファイルも表示できるようにしてあるので、JSON Schemaも表示できます。
$ shaft output docs schemas/ask.json
moreコマンドを使い日本語ドキュメントをページング処理で表示するとこんな感じ。

元々shaft output各コマンドは前方一致やglobで名前をフィルタリングできるようにしてあったので、shaft output docs jaと入力すると日本語のドキュメントを全て表示できたり、shaft output docs "*/getting_started.md"と入力すると英語と日本語両方のGetting Startedを表示できるようになりました。
おわりに
Shaftは一般的なコーディングエージェントと異なり参照ファイルを-r, --refオプションで渡すため、標準出力を参照してコマンドを実行するような機能がありませんが、CLIツールと同じリポジトリで管理していたドキュメントがこんな形で使えるようになるとは思いませんでした。
Gemini CLIやCodexのようなCLI型コーディングエージェントもドキュメントファイルを使えるようになればいいですね(2025/10/25現在は両方ともDocsコマンドないので)
おれい
Docsコマンド実装のきっかけになったyusukebeさんyamadashyさんありがとうございました。
犬好きという単語を見て絵文字を犬にしたのですが「Docsだからドッグなんですね!」みたいな幻聴が聞こえる程度には楽しめました。
Discussion