HTMLページをGitHubリポジトリの権限の範囲で公開/共有するgh-shareを作った

最近、コーディングエージェントに1ページのHTMLを書いてもらうことが増えました。

Pull Requestの変更点や、複雑なロジックを説明してもらったりなど。どれも一時的なものとして扱っていますが、Markdownで受け取るよりも、柔軟な描画が可能なHTMLでみた方がわかりやすいと感じています。私はそのためのSkill /one-page-html を用意していて、結構な頻度で使用します。 世の中にも、同じようなSkillやPluginがたくさんあるようです。

作成してもらったHTMLファイルは大抵、そのままブラウザで開いています。

ただ、時々チームにHTMLページをホスティングして共有したい場合があります。

当初は(私が所属しているテイラーが提供している)Tailor PlatformのStatic Website Hostingを使って共有していました(こういうとき自社プラットフォームを持っていると便利ですね!)。

github.com

これで全くもって十分だったのですが、もう少し汎用的なものが作れるかもと思ったらうまくできたので紹介します。

gh-share

github.com

gh-shareはローカルのファイルやディレクトリをリポジトリの権限の範囲で公開/共有できるGitHub CLIのエクステンションです。 gh extension install でインストールできます。

$ gh extension install k1LoW/gh-share

主な目的は、先ほども言ったように、1ページ分のHTMLの公開/共有です。

$ gh share pr123.html

基本はこれだけです。あとは待っていると、進捗が流れた後に共有用のURLが出ます。

$ gh share pr123.html

Successfully uploaded pr123.html.

╔══════════════════════════════════════════════════════════════════...
║ Branch:   https://github.com/k1LoW/octocov/tree/gh-share-staging (deleted)  
║ Commit:   https://github.com/k1LoW/octocov/commit/8f3c1d2a5b7e9f04c6a1b8d3e2f5a7c9b0d4e6f8 
║ Workflow: https://github.com/k1LoW/octocov/actions/runs/18234567890   
╚══════════════════════════════════════════════════════════════════...

Artifact URL:
https://github.com/k1LoW/octocov/actions/runs/18234567890/artifacts/456789

共有されたHTMLページやファイルは、そのGitHubリポジトリにアクセスできる人だけが閲覧できます。

ファイルを引数に(特にHTMLファイルを)渡すのが基本ですが、ディレクトリも渡せます。

$ gh share assets/

ファイルはそのままアップロードされ、ディレクトリは1つのzipファイルになります。

特定のリポジトリを指定して共有したいときは --repo を指定します。手元にGitリポジトリがなくても、動きます。

$ gh share --repo k1LoW/tbls report.html

アップロードが終わったらブラウザで開いてほしいときは --open オプションを追加してください。

$ gh share --open pr123.html

進捗やサマリはstderrに、共有URLだけがstdoutに出ます。なのでそのままパイプできます。

$ gh share pr123.html | pbcopy

URL以外の情報も機械的に扱いたい場合は --json があります。

仕組み

やっていることは少し変わっています。

gh-shareの出力URLから気づいている方もいるかもしれません。共有に使っているのはGitHub ActionsのWorkflow artifactsです。

Workflow artifactsはこの用途に都合のいい性質を持っています。ダウンロードにリポジトリへのアクセス権が必要なので、privateリポジトリならメンバーだけが見られます。retention policyで期限が来れば勝手に消えるので、一時的な共有物を掃除して回らなくてもよくなります。そして、リポジトリを使っている時点でそこにあるものなので、新たに用意するインフラはゼロです。

しかし、artifactを作るにはGitHub Actionsのワークフローを実行する必要があります。そしてワークフローは、そのワークフロー自身が存在するコミットでしか動作しません。

そこでgh-shareは、ステージング用のブランチ(デフォルトは gh-share-staging)をデフォルトブランチから作り、そこにpayload(共有するファイルやディレクトリ)と専用のワークフローをまとめてコミットします。pushをトリガーにワークフローが動いてartifactができあがったら、Artifact URLを出力後に(デフォルトの挙動では)ブランチを消します。

  1. gh repo view で対象リポジトリを解決する
  2. ステージングブランチがなければデフォルトブランチから作る
  3. payloadのblobとtreeをGit Database API経由で作る
  4. payloadとワークフローを1コミットにまとめてブランチを更新する
  5. ワークフローの完了を待ってartifactのURLを解決する
  6. ステージングブランチを削除する

ステージングブランチの操作に、ローカルのGitは一度も触っていません。すべてGitHub APIのblob / tree / commit / refを直接叩いて組み立てています。手元のリポジトリの状態がどうであっても、共有したいファイルさえあれば動きます。

1コミットにまとめているのにも理由があって、監視すべきコミットSHAが1つに定まりますし、1回の共有でワークフローが複数回起動する事故も防げます。

ワークフローのトリガーは .gh-share/payload-ref の1ファイルだけです。この設計のおかげで、後述するartifactの記録ファイルをブランチに書き足しても、余計なワークフローが実行されません。

artifactが消えても共有をやり直せる

artifactはretention policyで消えます。それは狙いどおりなのですが、たまに「あのHTMLページ、もう一度見たい」と思うことがあると思います。

そういったユースケースに対応するために、事前に --persist を付けて共有すると、ステージングブランチが消えずに残ります。

$ gh share --persist pr123.html

このとき、ブランチに .gh-share/artifacts/<artifact id>.json という記録が書かれます。artifact URLの末尾のIDから、それがどのpayloadから作られたのかを辿れる、という記録です。

そして --reshare にartifact URL(または末尾のID)を渡すと、その記録を読み込んでブランチ上のpayloadを再アップロードし、新しいartifact URLを発行します。

$ gh share --reshare https://github.com/k1LoW/gh-share/actions/runs/18234567890/artifacts/456789
$ gh share --reshare 456789

--persist を付けた場合、以降はステージングブランチはgh-shareによって消されることがなくなります。

後片付け

共有を繰り返していると、リポジトリにワークフローの実行履歴が蓄積していきます。後片付けをする場合は --purge オプションを使用します。

$ gh share --purge

これでgh-shareが作ったワークフローの実行履歴と、それに紐づくartifact、それから意図せず残ってしまったステージングブランチをまとめて消せます。なお、 --persist で残したブランチとデフォルトブランチは対象外なので、消えません。

まとめ

gh-shareを使うことで、コーディングエージェントが書いた1ページのHTMLを、そのリポジトリのメンバーにすぐに共有することができます。

gh-shareのメリットは、GitHub以外に何も不要なところです。すでにGitHub ActionsのWorkflow artifactsのストレージにアップロードしただけなので、アクセス制御や期限はリポジトリの設定がそのまま効きます。

似たようなことをやるツールとして toiroakr/mayfly があります。こちらはPull Requestとの連携や、残したいプレビューのGitHub Pages公開まで踏み込んでいるので、用途が近い方はあわせて見てみるとよさそうです。

是非使ってみてください。

macOS 26+のAPIを使ってリアルタイムに文字起こしと翻訳をするCLIとしてvoを作ってみた

TL;DR

  • Kanaryの会議録音+文字起こし+翻訳機能がカジュアルに使えてめちゃ便利
  • macOS 26+のAPIでリアルタイム翻訳機能が作れる

kanary.download

Kanaryが便利

Google Meetだと字幕機能は1言語のみをサポートしていて、実質「文字起こし」か「翻訳」かのどちらかの表示になってしまいます。 リアルタイムに「文字起こし」と「翻訳」の両方を出してくれるツールを探していました。

実際に使ってみて「これは便利!」と思いました。これがオフラインで動くのはかなり良いです*1。

是非皆さんも使ってみてください。

SpeechAnalyzer?

Kanaryのサイトをみていたら、「デバイス上の SpeechAnalyzer がタイムスタンプ付きの文字起こしに変換」という文章に気づきました。

一般的な単語ではないなあと思って調べてみたら、macOS が持つAPI だったようです。

developer.apple.com

「もしかして macOS が持つAPIだけでリアルタイム翻訳が実現できる?」

私はSwiftが全く書けないのですが、とても気になってしまったのでClaude Codeを使って作ってみました。

vo

github.com

voはリアルタイムに文字起こしと翻訳をするCLIツールです。

インストールはHomebrewのみサポートしています。

$ brew install k1LoW/tap/vo

使い方は次のようなイメージです。

$ vo                                  # マイクとスピーカーを対象にロケールの言語でリアルタイム文字起こし
$ vo --src en-US --dst ja-JP          # 英語をリアルタイム文字起こししつつ日本語にリアルタイム翻訳
$ vo --json | jq                      # JSONLで出力
$ vo --doctor                         # 環境診断

実際のリアルタイム翻訳の様子は次の動画をご覧ください(※音が出ます)。

vo のアーキテクチャとしては、マイクとシステム音声をそれぞれ SpeechTranscriber で文字起こしし、必要なら TranslationSession で翻訳する、完全オンデバイスの構成になっています。

システム音声は Core Audio のプロセスタップ で取り込むので、画面収録の権限は要りません。

2つの音源を並行に処理しつつ、順不同で返る翻訳結果を actor で厳密にソース順へ整流し、TTY か JSONL で出力します。

Apple Silicon + ローカルモデルすごい

普段使いなら、Kanary のほうが断然良いです。

ただ私個人としては、実際にコードベースで見てみてApple SiliconのNeural Engineの凄さ(これだけで実現できる)を体験できたことがよかったです。

これからも楽しみです。


ロケール違いに全く気づいていないところ教えていただいたので動画差し替えました!ありがとうございました!

*1:6月15日時点で、 https://kanary.download/ja/voice にリアルタイム翻訳機能について書かれていませんが機能としてはあります!

漢字練習のためのnpm packageを作成しているのですが、みなさんにお願いがあります

私は小学2年生の子を持つ*1のですが、小学生といえば漢字練習があります。

うちの小学2年生は文字の読み書きがあまり好きではないようで、漢字練習もやはり好きではないようです。

一方で、ちょっとでもゲーム要素が入ると、たとえそれが勉強っぽいものでも、ずっと楽しんでやっているという単純さもあります。

じゃあ、漢字練習にもゲーム要素を加えられたらずっとやるのではないか?と考えました。親も単純です。

おそらく「漢字を練習したらスタンプ1つ」みたいなレベルのものには「やりたくない」が勝ってしまい、見向きもしないので、もっとゲーム要素が必要です。

ところで、私はソフトウェアエンジニアです。

最近のタブレットはペンもついているし、タブレットで実際に漢字を書くタイプのゲームができればいいのではないかと考えました。漢字は書かないと覚えない気がするので。

SPAを作ってどこか(GitHub Pagesとか)でホストしたらタブレットから簡単にやってもらうこともできそうです。

というわけで、最近漢字練習のためのnpm packageをコツコツ作成しています。

kakitori

k1low.github.io

kakitori は「漢字や仮名の書き取り」を組み込むためのライブラリです。

できること

  • 漢字や仮名を、正しい筆順どおりに書けているかを画ごとに判定する
  • そのうえで「とめ」「はね」「はらい」まで判定する
  • 「学校」のような熟語を、マスに並べて1問の問題にする
  • マスにふりがなを振る
  • 「学[ ]」のように一部だけ書かせる穴埋め問題にする
  • 縦書きで複数の問題を並べて、漢字練習帳のページのようなレイアウトを組む
  • 書いた結果を取り出す(採点や記録などを想定)

3つのプリミティブ

kakitori は主に char、block、pageの3つのプリミティブで構成されていて用途に応じて使う設計です。

char

char はkakitoriの中の最小単位で1文字の書き取りを実現します。

import { char } from "@k1low/kakitori";

const c = char.create("学");
c.mount(document.getElementById("writer")!, {
  size: 300,
  showGrid: true,
  onCorrectStroke: (data) => console.log("OK", data.strokeNum),
  onMistake: (data) => console.log("NG", data.strokeNum),
  onComplete: ({ totalMistakes }) => console.log("done", totalMistakes),
});
c.start();

これで「学」が表示されて、正しい筆順で書かないと先に進めないというインタラクションができます。

block

block は1問の単位です。マス目とふりがなで構成されています。

import { block } from "@k1low/kakitori/block";

block.create(document.getElementById("host")!, {
  spec: {
    cells: [
      { kind: "guided", char: "学", mode: "write" },
      { kind: "guided", char: "校", mode: "write" },
    ],
    annotations: [
      { cellRange: [0, 1], expected: "がっこう", mode: "write" },
    ],
  },
});

「学」「校」のマスが縦に2つ並んだ問題が組めます。annotations に書いた「がっこう」が、ふりがな用のマスとして自動で寄り添う形で並びます。mode: "show" を指定すれば「お手本として字を見せるだけのマス」、mode: "write" で「書かせるマス」になるので、混ぜれば穴埋めもできます。

page

page は block を漢字練習帳のページに似た縦書きグリッドに構成することができます。

import { page } from "@k1low/kakitori/page";

page.create(document.getElementById("host")!, {
  writingMode: "vertical-rl",
  columns: 5,
  cellsPerColumn: 8,
  cellSize: 96,
  blocks: [
    { id: "q1", spec: { /* block の spec をそのまま */ } },
    { id: "q2", spec: { /* ... */ } },
    // ...
  ],
});

columns x cellsPerColumn の格子が用意されて、各 block を右上から流し込んでいく感じです。1つのblock が列をまたぐ場合の改行も自動でやってくれます。

「とめ」「はね」「はらい」判定

漢字の運筆判定そのものは Hanzi Writer という素晴らしい既存ライブラリの仕組みに乗っかっています。筆順どおりに線をなぞらせる、というところまではこれだけでできます。

kakitori 独自の機能として、その上に「とめ」「はね」「はらい」の判定を追加できるようにしています。文部科学省および文化庁は、「とめ」「はね」「はらい」について、「骨組みが適切であれば、正誤に影響しない」という見解(常用漢字表の字体・字形に関する指針)を示してはいるものの、字の練習という観点では大事だろうと判断して追加しています。

各画ごとに、ペンを離す直前の運筆の角度や速度を見て、判定しています。

書いた結果を取り出す

書いた結果も取得できます。

const result = p.result();
// {
//   complete: true,
//   matched: true,
//   blocks: [
//     { id: "q1",
//       cells: [
//         { kind: "guided", chars: [
//             { character: "学", complete: true, matched: true,
//               perStroke: [...], mistakes: 1, strokeEndingMistakes: 0 }
//         ]},
//         ...
//       ],
//       annotations: [...]
//     },
//     ...
//   ]
// }

「何文字書いたか」「画ごとの一致度はどれくらいか」「『とめ』を何回間違えたか」みたいな情報がツリー構造で返ってきます。collectCharResults() という補助関数を使うとページ全体の文字結果をフラットに取り出してフィルタすることもできるので、ゲーム的な採点処理にそのまま流し込めるはずです*2。

文字データのこと

文字データは @k1low/hanzi-writer-data-jp というforkedパッケージ経由で読み込んでいて、その元データは主に animCJK です。

animCJKは漢字の書き順や運筆情報を持つsvgファイルを作成するという狂気の1人プロジェクトです*3。すごい。感謝しかないです。

ただ animCJK が使っているフォントは小学校で習う字形とちょっと違うところがあります。たとえば「日」や「田」の中の横画が、フォントだと隣の縦画とちょっと離れているのですが、書き取り練習だとくっつけて書きます。「糸偏」や「竹冠」のように、筆順や形が日本の教科書と違う部首もあります。

どちらも正しいのですが、目的は「書き取り」なので、これらを日本の小学校で習う字形に直す subAnimJ というプロジェクトをはじめて animCJK と併用しています。あと、animCJKには数字(0〜9)が含まれていないので、数字用に animNumber というのも作って、これも取り込んでいます。

subAnimJやanimNumberをやりはじめて、さらにanimCJKの偉大さがわかります。subAnimJなどはまだまだ十分ではありません*4。引き続きやっていこうと思っています。

使うときのライセンス上の注意事項

kakitori 自体は実行時に文字データを fetch しているだけなのですが、kakitori を使ったWebアプリを公開する場合、デフォルト設定だと裏で @k1low/hanzi-writer-data-jp を経由して animCJK / subAnimJ / animNumber / Unihan由来のデータを利用することになるので、各上流プロジェクトのライセンスへの帰属表記をどこかに記載しておくのが良いと思います。

例となるHTMLスニペットを kakitori のREADMEに置いてあるので、よろしければ参考にしてください。

「とめ」「はね」「はらい」データのこと

「とめ」「はね」「はらい」を判定するためには「ここはとめる」「ここははねる」「ここははらう」というラベルを、各文字の各画につけたデータが必要になります。これがいい感じのオープンデータとして見つからなかったので、自分で作ることにしました。

それが @k1low/kakitori-data というパッケージです。

たとえば「あ」のデータはこんな感じになっています。

{
  "character": "さ",
  "strokeEndings": [
    { "types": ["tome"] },
    { "types": ["tome", "hane"] },
    { "types": ["tome"] }
  ]
}

strokeEndings[i] が i 画目の終わり方を表しています((types を配列にしているのは、字によっては「とめ」も「はね」も許容されるケースがあるためです))。

現在の進捗としては、ひらがなと数字(0〜9、全角・半角)はだいたい埋まったのですが、漢字はまだほぼ手付かずです*5。これもコツコツやっていこうと思っています。

このデータは正解がない(というか正解は「とめ」でも「はらい」でも「はね」でもいいっぽい)ので、他の人からの貢献を受け付けにくいなあと悩んでいます。

「正解がないのはわかっているが、それでも文字を綺麗に書くためにとめはねはらいをしっかり意識して欲しい」という「思い」からの機能なのです。

なお、@k1low/kakitori-data にデータがなければ判定をしないし、そもそも判定を外すこともできるので、安心してください。

ところで、根本的な問題にぶつかっている

ここまで一見すると順調なプロジェクトなのですが、実は、今、根本的な問題にぶつかっています。結構前から気づいていたのですが、いまだ解決の糸口が見つかっていない問題です。

それは、 私にゲームを作るセンスどころか能力が全くないこと です。

「kakitori でレイアウトもふりがなも結果取得もできるんだから、あとはそれっぽく組み合わせればゲームになるだろう」と最初は思っていたのですが、全く進みません。

何をどうしたらゲームが作れるのか、全く手が出ないのです。

「ゲーム要素を加えればやってくれるはず」という発想で kakitori を作り始めたのに、その「ゲーム要素」が私からは生み出せない、というのが今です。厳しい。

みなさんにお願い

「漢字書き取り」に同じような課題を持つみなさん。

kakitori は頑張って作っていきます(すみません。まだAPIもガンガン変えると思います)。

なので、もし、みなさんが kakitori を使って漢字ゲームを作ったら私にも教えていただけないでしょうか。子にやらせたいです。

どうかよろしくお願いいたします。私も引き続きめげずにゲームを考えてみます。

*1:2人目が今月生まれました👶

*2:まだ、BREAKING CHANGE はあると思います

*3:Issueのコメントをみる限り、1人プロジェクトに見えます

*4:主に調整したのが小学校3年生の漢字までです

*5:執筆時点で 76 / 2270 文字

待つツールを作って活用している ( gh-wait / gh-copilot-review )

最近はCoding Agentを使って開発をしています。複数のCoding Agentを立ち上げて、それらと複数のタスクを並行して進めるようになりました。

一方で、感覚として「待つ」ことが多くなった気がします。

PRを出してCIの完了を待つ、レビューを待つ、マージを待つ。IssueやDiscussionでの回答を待つ。並行して進めていると「あれどうなったかな」とターミナルだけではなくブラウザのウィンドウを行ったり来たりする回数が増えています。

そして、Coding Agentは「待つ」のが苦手っぽい?気がしています*1。

Coding Agentの作業フローに「状態の変化を待つ」を組み込みにくいんですよね。待ちが入る作業をあまりうまくやってもらえていない。ここに改善ポイントがありそうです。

最近は「待つ」ツールを作って活用しています。

gh-wait

github.com

gh-wait はGitHub CLIのエクステンションで、Pull Request、Issue、Discussion、Workflow Runを監視して、指定した条件が満たされたときにアクション(ブラウザを開く、デスクトップ通知など)を実行するツールです。

基本的な使い方は、GitHubのURLをそのまま渡すだけです。対象の種類(PR、Issue、Workflow Runなど)は自動で判別してくれます。

# PRが承認されたらブラウザで開く
$ gh wait https://github.com/owner/repo/pull/123 --approved --open

# CIが完了したらデスクトップ通知
$ gh wait https://github.com/owner/repo/pull/123 --ci-completed --notify

# Workflow Runの完了を待つ
$ gh wait https://github.com/owner/repo/actions/runs/23424874935 --completed --notify

本当は gh wait pr や gh wait workflow のようなサブコマンドもあるのですが、大抵はURLを渡して使っています。GitHubの画面からURLをコピーしてそのまま貼るだけなので楽です。

継続監視

デフォルトでは条件が1回成立したらルールが削除されますが、 --until や --count で継続的に監視できます。

$ gh wait pr --commented --notify --until merged

この例だと、PRにコメントが付くたびにデスクトップ通知を出して、マージされたら監視を終了します。

アーキテクチャ

クライアント・サーバ方式を採用しています。初回のルール作成時にバックグラウンドサーバが自動起動して、各ルールの設定間隔でGitHub APIをポーリングします。ルールは永続化されるのでサーバを再起動しても大丈夫です。

自分自身のイベント(自分のコメントや承認)は自動的にフィルタされるので、自分でコメントして自分に通知が飛ぶ、みたいなことは起きません(地味に大事)。

gh-copilot-review

github.com

gh-copilot-review もGitHub CLIのエクステンションです。PRに対してGitHub Copilotのコードレビューをリクエストします。

「それって gh pr edit --add-reviewer @copilot でよくない?」と思うかもしれませんが。実際にはCopilotレビューを連続的に活用しようとすると、いくつか面倒なことがあります。

  • 古いコメントが残る
    • コミットを積むたびにCopilotにレビューをリクエストすると、前回のレビューコメントがPR上に残って見づらくなる
  • レビュー完了のタイミングがわからない
    • Copilotのレビューには時間がかかるので、完了を知りたい。しかし(意外に)完了を判断するのにコツがいる(失敗するとCopilotのレビューに対応している間にCopilotからのレビューが追加される)

gh-copilot-review はこれらをコード化しています。

古いコメント(インラインのレビューコメントを除く)を自動的に非表示(minimize)にしてから、新しいレビューをリクエストします。すでにレビュー済み・レビュー中の場合は不要なリクエストをスキップします。

基本的にはPull requestを作業中のブランチで実行するだけです。

$ gh copilot-review
Minimized 3 outdated Copilot review(s)
Copilot review requested on PR #42

そして、 --wait オプションでCopilotのレビュー完了を待機できます。

$ gh copilot-review --wait
Minimized 1 outdated Copilot review(s)
Copilot review requested on PR #42
Waiting for Copilot review... (30s elapsed)
Waiting for Copilot review... (1m0s elapsed)
Copilot review completed on PR #42

タイムアウトとポーリング間隔もカスタマイズできます。

$ gh copilot-review --wait --wait-timeout 5min --wait-interval 10sec

私の場合、Copilotからのレビューがなくなるまで何度もレビューを依頼したりします*2。

今のところは人間のためのツール

gh-wait も gh-copilot-review も、今のところは人間が状態の変化に気づくために使っています。

Coding Agentの作業フローに直接組み込めたらもっと良くなるとは思っていて、エージェントに「Copilotにレビューをリクエストして、完了を待って、レビューがあればそれぞれ対応を提案して*3人間の判断をもとに対応して。それを繰り返して。」という指示を出せるようになるのが理想です*4。

例えば、「待つ」部分とCoding Agentに頑張ってもらう部分を明確に分けると、うまくいくのかもしれない。そう考えて、実は実験的に runn に Agent runnerを追加していて、runnのシナリオとして「待つ」と「エージェントに作業させる」を組み合わせて、開発のルーティーンを記述できないかなあと模索中です。

# 現時点でのAgent runnerのインターフェイス
runners:
  claude:
    agent: claude
    model: claude-sonnet-4-20250514
    system: "You are a helpful assistant."
    permissions:
      - "allow:*"
steps:
  -
    claude:
      prompt: "What is Go?"
    test: current.res.content != ''

gh-wait は汎用的な「待つ」ツール、 gh-copilot-review はCopilotレビューに特化した「レビューを依頼して待つ」ツール。どちらもGitHub CLIのエクステンションなので、インストールは同じです。

$ gh extension install k1LoW/gh-wait
$ gh extension install k1LoW/gh-copilot-review

よければ使ってみてください。

*1:もしかしたら私が知らなくてうまくいっていないだけかもしれない

*2:最近のCopilotのレビューは優秀です

*3:ここは https://github.com/k1LoW/gh-pr-reviews という別のツールを作っています

*4:まだうまく回った試しがない

ブラウザベースのMarkdown viewerとしてmoを作った

OpinionatedなMarkdown viewer

世の中にMarkdown viewerはたくさんありますが、自分がほしかったのは以下の特性を持つものでした。

  • CLIからMarkdownを開きたい
  • Markdownはブラウザで見たい(ローカルWebサーバ方式)
  • Markdownを開くたびに別プロセス/別ポートが使用されないようにしたい
  • グルーピングもしたい

また、コーディングエージェントがMarkdownドキュメントを書くようになり、CLIからファイルを追加できることがキーになってきました。 エージェントの行動を邪魔しないインターフェイスが求められています。

というわけで、OpinionatedなMarkdown viewerを作ってみました。

mo

github.com

mo は Markdownファイルをブラウザで openするCLIツールです。Markdownファイルを渡すとブラウザが開いてレンダリングされたドキュメントが表示されます。

(Go + 組み込みReact SPAの)単一バイナリで、依存関係なしにインストールできます。

使い方

基本はファイルを渡すだけです。

$ mo README.md
mo: serving at http://localhost:6275 (pid 12345)

複数ファイルも渡せます。

$ mo README.md CHANGELOG.md docs/*.md

グループ

--target (-t) フラグでファイルをグループに分けられます。グループごとにURLパスとサイドバーが分かれます。

$ mo spec.md --target design      # http://localhost:6275/design
$ mo api.md --target design       # design グループに追加
$ mo notes.md --target plans      # http://localhost:6275/plans

設計ドキュメントは"design"グループ、コーディングエージェントが書いたプランファイルは"plans"グループ、という使い分けもできます。

moの仕組み

mo は結構私のユースケースに合わせて特徴的な動きをします。

mo はデフォルトでバックグラウンドでサーバーを起動します。最初の mo コマンドでサーバーが立ち上がり、シェルはすぐに返ってくるので、そのまま作業を続けられます。

$ mo README.md
mo: serving at http://localhost:6275 (pid 12345)
$ # シェルがすぐ使える

これが個人的にめちゃくちゃ使いやすいです。

コーディングエージェントにも使わせやすいですし、スクリプトにも組み込みやすいです。ブラウザオープンも --open、 --no-open オプションで制御可能です。

2回目以降の mo コマンドの実行も、同じポートですでにサーバーが動いている場合新しいサーバーは起動せず既存のサーバーにファイルを追加します。

$ mo README.md          # サーバー起動してファイル追加
$ mo CHANGELOG.md       # 既存のサーバーにファイル追加
$ mo docs/*.md          # さらに追加

つまり、mo コマンドを叩くたびにプロセスやポートが増えていくことはありません。1つのサーバーに対してファイルをどんどん追加していく設計です。

Webアプリケーションを1つ起動してそこにファイルを追加していくイメージです。Webアプリケーションを開発した経験のある私には馴染みのある動きです。

なお、完全に別のセッションが必要な場合は別ポートを使えば可能です。

$ mo draft.md -p 6276

サーバーの管理もCLIから行えます。これもWebサーバを管理したことがある人には馴染みがあるのではないでしょうか。

$ mo --status              # 稼働中のmoサーバーを一覧表示
$ mo --shutdown            # デフォルトポートのサーバーをシャットダウン
$ mo --shutdown -p 6276    # 特定ポートのサーバーをシャットダウン

Web UIからサーバーをリスタートすることもできます。

画面右下のリスタートボタンをクリックすると、開いているファイルやグループの構成を保ったままサーバーが再起動します。

mo をバージョンアップした後、ファイルを開き直すことなく新しいバージョンに切り替えられるので便利です。

Viewerとしての機能

Markdownのレンダリングは基本的なものには対応しています。

  • GitHub Flavored Markdown(テーブル、タスクリスト、脚注など)
  • Shiki によるシンタックスハイライト
  • Mermaid の図表レンダリング
  • GitHub Alerts(admonitions)

UI

UI周りの機能としては以下があります。

  • ダーク / ライトテーマ切り替え
  • 目次パネル(右側)
  • Rawマークダウン表示の切り替え
  • コンテンツコピー(Markdown / テキスト / HTML形式を選択可)
  • ドラッグ&ドロップでのファイル並び替え

UIはGitHubのMarkdownレンダリングに寄せたデザインにしています。最終的にGitHubでどう見えるかがわかることが重要ですし、何よりGitHubの見た目が一番慣れているので読みやすいです。

サイドバーのファイル一覧には、フラットビューとツリービューの2つの表示モードを用意しています。

フラットビューはファイル名だけを並べたシンプルな一覧で、ドラッグ&ドロップで自由に並び替えられます。少数のファイルをサッと確認したいときはこれで十分です。

一方、docs/*.md のように同じディレクトリの下に大量のファイルを開いた場合、フラットビューだと一覧が長くなって見づらくなります。ツリービューに切り替えると、ディレクトリ階層で折りたたんで表示できるので、ファイルが多くても整理しやすくなります。

ライブリロード

mo は開いたファイルの変更を監視しています。

ファイルが更新されると、SSE(Server-Sent Events)経由でブラウザに通知が飛び、自動でコンテンツが再取得・再描画されます。

アーキテクチャ

GoのHTTPサーバーにReact SPAを go:embed で組み込んでいます。go generate でフロントエンドをビルドしてから go:embed でバイナリに埋め込むので、最終成果物は単一バイナリになります。

バックエンドはGoで、ファイル管理・API・SSE・ファイル監視を担当しています。フロントエンドはVite + React + TypeScript + Tailwind CSSで、react-markdownを使ってMarkdownをレンダリングしています。

インストール

homebrew tap:

$ brew install k1LoW/tap/mo

manually:

releases page からダウンロードできます。

go install は残念ながらできません。

References

mo のアイデアは yusukebe/gh-markdown-preview をベースにしています。gh-markdown-previewはずっと愛用してきたツールで、CLIからMarkdownをブラウザでプレビューするという体験の原点です。そこに自分がほしかった機能を足していった結果、別のツールとして形になりました。

まとめ

mo を使うことで、複数のMarkdownファイルを持続的にプレビューできるようになりました。

特に気に入っているのは、バックグラウンドでサーバーが動いていて、mo コマンドでファイルをどんどん追加できるところです。シェルを掴まないのも推しポイントです。

是非使ってみてください。

octocov.dev でカバレッジレポートを確認する

octocovはコードメトリクス(カバレッジ、Code to Test Ratio、テスト実行時間)を収集するツールキットです。以前もこのブログで紹介しました。

k1low.hatenablog.com

k1low.hatenablog.com

今回、octocovのサイトとして octocov.dev を作りました。

octocov.dev

ランディングページとしてoctocovの紹介(ほぼリンクだけ)を掲載しつつ、実験的機能としてカバレッジレポートをWebブラウザで閲覧できる機能も載せています。というか、これが作りたくて octocov.dev を作りました。

カバレッジレポート閲覧機能

octocovには artifact:// というdatastore設定があり、GitHub Actionsのアーティファクトにレポートを保存できます。外部サービスのアカウント不要で使えるので、個人的にはこれがお気に入りです。

CI上ではPRコメントやJob Summaryでレポート自体は確認できるので普段は困らないのですが、「任意のタイミングでサッと確認したい」「任意のソースコードのカバレッジを行単位で確認したい」となるとCLIとしての octocov コマンドを使用するしかありませんでした。

octocov.devでGitHubにサインインすると、artifact:// datastoreに保存されたカバレッジレポートをブラウザで閲覧できます。

前提として、リポジトリに以下のような .octocov.yml が設定されている必要があります。

# .octocov.yml
report:
  datastores:
    - artifact://owner/repo

そして octocov-action を使ってGitHub Actionsのアーティファクトにレポートが保存されている必要があります。

github.com

使い方

サインイン後、ヘッダーの検索バーに owner/repo を入力するとレポートページに遷移します。

レポートサマリでは、コードカバレッジ、Code to Test ratio、テスト実行時間、そしてファイル一覧が表示されます。

ファイル一覧からファイルをクリックすると、ソースコードビューに遷移します。行ごとのカバレッジが色分けで表示されます(緑=カバー済み)。シンタックスハイライトも有効なので、コードも読みやすいと思います。

スコープとプライバシー

サインイン時に public_repo(パブリックリポジトリのみ)と repo(プライベートリポジトリも含む)のスコープを選択できます。

データの取り扱いについて明確にしておくと、サーバーが保持するのはセッション(GitHubアクセストークンとユーザー情報)のみです。カバレッジデータはブラウザのIndexedDBに、ソースコードはキャッシュせず毎回GitHub APIから取得します。ログアウト時にはブラウザキャッシュを全削除します。

技術スタック

バックエンドはCloudflare Workers + Hono、フロントエンドはReact + Tailwind CSS v4で、@cloudflare/vite-plugin を使って単一のViteビルドに統合しています。シンタックスハイライトにはShikiを使用しています。

認証はGitHub OAuth + PKCEで、セッション管理にCloudflare KV(8時間TTL)を使っています。

パフォーマンス確保のため、クライアント側ではIndexedDBにカバレッジデータをキャッシュしています。

一方、サーバー側にはカバレッジデータのキャッシュは持たせていません。また、ソースコードはどこにも持たせていません(都度フェッチ)。これはプライベートリポジトリのデータが意図せず漏洩するリスクを避けるためです。

まとめ

octocov CLIはターミナルで、octocov-actionはCIで、そしてoctocov.dev はブラウザで。octocovのカバレッジレポートを確認する手段がまた一つ増えました。

CoverallsやCodecovのような専用カバレッジサービスの代替ではなく、octocovの artifact:// datastoreを使っているユーザー向けの軽量なビューアという位置づけです。

よかったら使ってみてください。

tmuxのウィンドウで動かしているコーディングエージェントのステータスを確認できるtcmuxを作った

普段使っているtmuxにClaude Codeの情報がほしい

私は普段からtmuxを使っています。Emacsを開いているウィンドウ、シェルを開いているウィンドウ、特定の作業をしているブランチのウィンドウ、別のリポジトリのウィンドウといった感じで複数のウィンドウを行き来しながら作業しています。

最近はClaude Codeを使うことが増えてきて、tmuxのウィンドウのいくつかでClaude Codeが動いている状態になりました。あるウィンドウではバグ修正を依頼して、別のウィンドウでは新機能の実装検討を依頼して、さらに別のウィンドウではテストを書かせている、といった具合です。

ここで問題がひとつ。「どのウィンドウのClaude Codeが今どういう状態なのかがわからない」。

Claude Codeは、処理中なのか、完了して入力待ちなのか、あるいは許可を求めて止まっているのか、実際にそのウィンドウに切り替えないとわかりません。許可を求めて止まっているのに気づかず放置してしまうこともあります。

Claude Code専用の管理ツールを作るという手もありますが、自分がやりたいのは 普段使っているtmuxにClaude Codeの情報がのっている状態 です。Emacsやシェルといったウィンドウはそのままでいいので、Claude Codeが動いているウィンドウだけステータスが見えればいい。

というわけで、tmuxのウィンドウ一覧にClaude Codeのステータスを表示するツールを作りました。

tcmux

github.com

当初は "terminal と Claude Code の mux viewer" ということで tcmux という名前にしましたが、GitHub Copilot CLIにも対応したので、今は "terminal coding agent mux viewer" ということにしています。tmuxのウィンドウ一覧やセッション一覧にコーディングエージェントのステータスを付加して表示するCLIツールです。

それだけのツールです。

使い方

list-windows (lsw)

tcmux list-windows でClaude Codeが起動しているウィンドウの一覧を表示できます。

$ tcmux list-windows
0: editor (1 panes) ✻ Fix login bug [Idle]
2: server (2 panes) ✻ Add API endpoint [Running (1m 30s)], ✻ Write tests [Idle]
5: docs (1 panes) ✻ Update README [Idle]
7: review (1 panes) ✻ Review PR [Waiting]

各行の末尾にClaude Codeのステータスが表示されます。これで、どのウィンドウが処理中で、どのウィンドウが入力待ちかが一目でわかります。

-A オプションをつけると、Claude Codeが起動していないウィンドウも含めて全て表示されます。

$ tcmux list-windows -A
0: editor (1 panes) ✻ Fix login bug [Idle]
1: shell (1 panes)
2: server (2 panes) ✻ Add API endpoint [Running (1m 30s)], ✻ Write tests [Idle]
3: logs (1 panes)
4: htop (1 panes)
5: docs (1 panes) ✻ Update README [Idle]
7: review (1 panes) ✻ Review PR [Waiting]

-F オプションには tmux list-windows と互換があるので自由にフォーマット変更が可能です。

オプション 説明
-A, --all-windows Claude Codeが起動していないウィンドウも含めて全て表示
-a, --all-sessions 全セッションのウィンドウを表示
-t, --target-session 対象セッションを指定
-F, --format 出力フォーマットを指定(tmux互換+tcmux拡張)

list-sessions (ls)

tcmux list-sessions でtmuxセッションの一覧を、Claude Codeのステータス集計とともに表示できます。

$ tcmux list-sessions
dev: 7 windows (attached) - 3 Idle, 1 Running, 1 Waiting
main: 2 windows - 1 Idle
work: 1 window

複数のセッションを使い分けている場合に便利です。

ステータス検出

tcmuxは以下のステータスを検出します。

ステータス 説明
Idle プロンプト待ち。Claude Codeが次の入力を待っている状態
Running 処理中
Waiting ユーザー入力待ち。許可ダイアログや確認プロンプトが表示されている状態

また、モードも検出して表示します。

モード 説明
plan mode Plan modeがONの状態
accept edits Accept editsがONの状態

例えば、Plan modeがONで処理中の場合は [Running (30s, plan mode)] のように表示されます。

GitHub Copilot CLIにも対応した

tcmuxはClaude Code向けに作り始めましたが、ターミナルで動くコーディングエージェントはClaude Codeだけではありません。私は、主にレビュータスク目的でGitHub Copilot CLIも同様にtmux上で動かすことがあります。

そこで、GitHub Copilot CLIのステータス検出にも対応しました。検出するステータス(Idle / Running / Waiting)はClaude Codeと同じです。エージェントの種類はアイコンで区別できます。

アイコン エージェント
✻ Claude Code
⬢ GitHub Copilot CLI

Claude CodeとCopilot CLIを混在させて使っている場合はこんな感じの表示になります。

$ tcmux list-windows
0: editor (1 panes) ✻ Fix login bug [Idle]
2: server (2 panes) ✻ Add API endpoint [Running (1m 30s)], ⬢ Write tests [Idle]
5: docs (1 panes) ⬢ Update README [Running]
7: review (1 panes) ✻ Review PR [Waiting]

レシピ

自分が実際に使っている設定を紹介します。

tmuxのウィンドウ選択で 、.tmux.conf で tmux list-windows の代わりに tcmux list-windows -A を使うと、ウィンドウ選択時にClaude Codeのステータスが表示されて便利です。

Before:

bind-key w run-shell "tmux list-windows | fzf --tmux | cut -d: -f1 | xargs tmux select-window -t"

After:

bind-key w run-shell "tcmux list-windows -A --color=always | fzf --ansi --tmux | cut -d: -f1 | xargs tmux select-window -t"

基本 tmux list-windows の置き換えのためだけに作ったので実質これが全てかもしれません。

Waitingのウィンドウがあれば一目でわかるので、許可を求められているClaude Codeを放置してしまうことがなくなりました。

自分はもう少し見た目を調整して使っています。

bind -r w run-shell "tcmux lsw -A --color=always | fzf --ansi --layout reverse --tmux 80%,50% --color='pointer:24' | cut -d: -f 1 | xargs tmux select-window -t"

冒頭のポストのイメージのように表示されます。

インストール

homebrew tap:

$ brew install k1LoW/tap/tcmux

go install:

$ go install github.com/k1LoW/tcmux@latest

まとめ

tcmuxを使うことで、tmux上で複数のClaude Codeを並行して動かしているときに、各インスタンスの状態を把握しやすくなりました。

特に「許可待ちで止まっているClaude Codeに気づかない」問題が解消されたのが大きいです。GitHub Copilot CLIにも対応しているので、複数のコーディングエージェントを混在させて使っている場合でも、アイコンで区別しつつ同じインターフェースでステータスを確認できます。