はじめて OpenCode プラグインを作りました。名前は opencode-autopilot-logbook です。セッションがアイドルになったら、勝手に日報 YYYYMMDD_logbook.md を作ってくれる、ちょっと気が利くプラグインです。
正直に言います。作るのは楽しかったのですが、公開までの道のりで何度もこけました。公式ドキュメントを読んだだけではわからない「地味な落とし穴」がいくつもあったのです。
この記事では、計画書と Git 履歴を振り返りながら、後から続くエンジニアの方が同じところで迷わないよう、具体的なコマンドと失敗例つきで残します。
成果物
今回作成したプラグインの成果物はこちらです。
インストール方法
npm install -g opencode-autopilot-logbook
opencode plugin opencode-autopilot-logbook -g
セットアップ方法
- OpenCode を再起動します。環境変数は起動時に読み込まれます
- セッションで少し作業して、アイドル状態にします
- 既定では
artifacts/daily/YYYYMMDD_logbook.mdに日報が作られます
出力先を変えたいときは、起動前に環境変数を設定します。
# 一時的に変える
export OPENCODE_DAILY_LOGBOOK_OUTPUT_DIR="daily"
opencode
# ずっと変えておく(zsh の例)
echo 'export OPENCODE_DAILY_LOGBOOK_OUTPUT_DIR="daily"' >> ~/.zshrc
source ~/.zshrc
あわせて読んでほしい
私たちが大切にしている OSS への向き合い方を、指針としてまとめたコラムです。社会に積み重なった見直されない仕組みを「技術的負債」と捉え直す、当社のOSS活動の背景にある考え方に触れていただけるとうれしいです。
- 「昔はそうだったから」で裁かれる、【リキッド通販ショップ】
まず、このプラグインは何をするものか
一言でいうと「自動で作業日報が作成される」プラグインです。
- OpenCode の
session.idleというイベントをきっかけに動きます - 直前の会話を要約し、テンプレートに流し込んで Markdown を作ります
- 出力先は既定で
artifacts/daily、環境変数で変えられます - テンプレートも環境変数で差し替えできます
中身は 1 ファイル daily-logbook.ts が本体で、bun build で dist/index.js に束ねて配ります。シンプルな構成ですが、シンプルだからこそ「配り方」でつまずきました。
全体の流れを Git 履歴で振り返る
Git ログをそのまま追うと、物語が見えてきます。
afb1952 v1.0.7 OpenCode plugin 対応、出力先変数追加
0686e7e daily-logbook.ts をルートに移動、ビルドパス更新
c5bbcf5 .gitignore に node_modules 追加
2417882 日本語を英語に置換
7986c41 v1.0.9
937d57e スキル追加 opencode-plugin-publish
885f3ec docs 手動コマンド主張を削除
e645274 feat マスキング、スロットリング、daily limit
cc70f0a build dist/index.js 再ビルド
3dcaabb 細かい PAT と JWT のマスキング追加
4d3c186 JWT の正規表現に上限を付与
32bd673 並行生成の抑止とパスの整列
e85c141 README に制限事項を明記
fd0bb71 日付の注入とエラー経路のテスト
1788526 テンプレート移設とレビュー文書の確定
プラグインの正しい形。ここを間違えると動きません
OpenCode プラグインの package.json には、決まったお作法があります。私は最初、なんとなくで書いて痛い目を見ました。
{
"name": "opencode-autopilot-logbook",
"version": "1.1.0",
"type": "module",
"main": "dist/index.js",
"files": ["dist", "README.md", "README.jp.md", "CHANGELOG.md"],
"scripts": {
"build": "bun build daily-logbook.ts --target=bun --outfile dist/index.js",
"prepublishOnly": "bun run build"
},
"peerDependencies": { "bun": ">=1.0.0" }
}
つまずき 1: main は dist/index.js でなければいけません。exports ではだめでした
exports フィールドでなんとかなるだろうと思っていたのですが、OpenCode 側は main を見ます。main が dist/index.js を指していないと「プラグインが見つからない」となります。地味ですが致命的です。
つまずき 2: type は module が必須です
付け忘れるとビルド成果物の読み込みでこけます。ESM として扱われることが前提になっています。
つまずき 3: files に dist を入れ忘れると、npm に空っぽが届きます
files は「npm に含めるもの」リストです。ここに dist がないと、せっかくビルドした index.js が公開物に入りません。npm pack --dry-run で毎回確認する癖をつけて助かりました。
npm pack --dry-run
# 5 files だけ並べば正解
# README.md / README.jp.md / CHANGELOG.md / dist/index.js / package.json
つまずき 4: prepublishOnly でビルドを自動化しないと、古い dist を公開します
手元で npm run build し忘れたまま npm publish すると、古いコードがそのまま世に出ます。prepublishOnly に bun run build を入れておくと、公開直前に必ず最新が作られます。私はこの一行に何度も救われました。
ビルドと成果物でつまずいた 2 つ
つまずき 5: OpenCode のキャッシュが古いバージョンを掴み続けました
プラグインを更新しても、OpenCode 側が ~/.cache/opencode/packages/opencode-autopilot-logbook* を掴んだままになることがあります。「No plugin targets found」と出たら、まずキャッシュを疑ってください。
rm -rf ~/.cache/opencode/packages/opencode-autopilot-logbook*
opencode plugin opencode-autopilot-logbook -g
README にも書きましたが、この一行を知っているかどうかで、30 分溶けるかどうかが決まります。
つまずき 6: ビルドは bun build、テストも bun test に寄せると楽でした
当初、テストを node:test で書こうとしました。ところが daily-logbook.ts は TypeScript で、古いNodeではそのまま実行できません。素直に bun test に寄せました。bun は peerDependencies で必須なので、追加依存も要りません。
"scripts": { "test": "bun test" }
bun test
# 45 pass 0 fail まで育ちました
テスト対象は maskSecrets や isWithinWindow など、副作用のない純粋関数を export して切り出しました。副作用を外に出すと、テストが一気に書きやすくなります。
機能でつまずいた 4 つ。ここが一番学びが多かったです
つまずき 7: シークレットのマスキングは「切り詰める前」にやらないと漏れます
会話の transcript をそのままプロンプトに埋め込むと、sk-... や ghp_... などの秘密が流れてしまう可能性があります。そこで maskSecrets で *** に置き換えるのですが、順番を間違えると漏れます。
悪い順序
- 長い transcript を 12,000 文字で切り詰める
- そのあとマスキング
切り口で sk- が分断されると、正規表現が当たらず 2 文字だけ残ってしまうのです。正しい順序は逆でした。
良い順序
- まずマスキング
- そのあと切り詰める
const maskedTranscript = isRedactEnabled() ? maskSecrets(transcriptLines) : transcriptLines
return truncateText(maskedTranscript, TRANSCRIPT_MAX_CHARS)
テストでも「切り口で sk が残る」ケースをわざと作り、マスキングを後にすると落ちることを確認しました。小さな順番の違いが、安全を分けます。
対象パターンは OpenAI 形式の sk-/SK-、Bearer、AWS の AKIA、GitHub の ghp_ や github_pat_、Slack の xoxb-、JWT の eyJ...、PEM の秘密鍵ブロック、password: のようなキー値ペアなどです。ただし完全な保護を保証するものではないので、README に「フェイルセーフであり、機密はそもそも prompt に入れない運用が前提」と明記しました。
つまずき 8: 日付またぎで「きょう」の日報が「あした」になることがありました
buildPrompt の中で new Date() をもう一度呼んでいると、存在チェックは 23時59分、プロンプト生成は 0時01分、ということが起きます。daily-limit の「1日1回」が、日付のズレで壊れるのです。
直し方はシンプルで、イベント処理の入口で now = new Date() を 1 回だけ作り、その now を buildPrompt に渡すようにしました。同じ now を使い回すことで、チェックと生成とタイトルが同じ日付で揃います。
const now = new Date()
const { date } = formatDateTokens(now)
const prompt = buildPrompt(template, sessionId, transcript, includeTranscript, promptOutputDir, now)
深夜に動くプラグインだからこそ、0 時をまたぐ想定は外せませんでした。
つまずき 9: 別セッションが同時にアイドルになると、2 回作られてしまいました
daily-limit は「きょうのファイルがすでにあるなら作らない」というファイル存在チェックで実現しています。ところが inFlightSessionIds はセッション単位のガードなので、別のセッションが同時にアイドルになると、両方が「まだファイルはない」と判断して二重に作ってしまうのです。
メタレビューで指摘されるまで、私も見落としていました。直し方は、日付キーのグローバルガードを足すことでした。
const dailyLimitInFlightByDate = new Set<string>()
// daily-limit 有効時のみ
if (isDailyLimited && dailyLimitInFlightByDate.has(date)) {
return
}
dailyLimitInFlightByDate.add(date)
try {
// 生成処理
} finally {
dailyLimitInFlightByDate.delete(date)
}
ファイルができれば次回は existsSync で止まりますが、まだファイルができる前の「同時」の瞬間を、このメモリ上のガードで抑えます。気づくまで、なぜ 1 日 1 回のはずが 2 回できたのか、首をかしげていました。
つまずき 10: outputDir のパス解決が、チェックと生成でズレていました
存在チェックは resolve(directory, outputDir, ...) で、プラグインの directory を基準に絶対パスで見ます。ところがプロンプトに渡す {{ outputDir }} が相対パスの artifacts/daily のままだと、エージェント側は別のディレクトリを基準に書いてしまい、チェックと実ファイルの場所がズレます。
daily-limit 有効時だけ、プロンプトに渡す outputDir も絶対パスに直して渡すようにしました。無効時は従来の相対パスのままにして、後方互換を保っています。
const promptOutputDir = isDailyLimited ? resolve(directory, outputDir) : outputDir
地味な差ですが、運用では「日報が見つからない」という混乱になります。気づけてよかったです。
おまけのつまずき 正規表現の {10,} が、長い入力で重くなることがありました
JWT の正規表現を /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g と書いていたところ、測ってみると 500KB の意地悪な入力で少し重くなりました。QA で指摘され、各セグメントに上限 {10,120} を付けました。実用上の JWT は 120 文字を大きく下回るので、安全マージンとして十分です。20 ミリ秒未満に収まることを実測で確認しました。
公開までのチェックリスト。
私が最後にやった確認を、そのまま置きます。コピペで使えます。
# 1 テストとビルド
bun test
npm run build
git status
# dist/index.js に差分がないことを確認
# 2 公開物の中身
npm pack --dry-run
# 5 files だけか、旧 daily-logbook.js が混ざっていないか見る
# 3 バージョンと履歴
# package.json の version と CHANGELOG が一致しているか
# 4 キャッシュの掃除(動作確認時)
rm -rf ~/.cache/opencode/packages/opencode-autopilot-logbook*
opencode plugin opencode-autopilot-logbook -g
# 5 公開
npm login
npm publish --access public
振り返って、思うこと
はじめてのOpenCode プラグイン作りで、一番感じたのは「公開情報が不十分」ということでした。もしあなたが次に OpenCode プラグインを作るなら、この記事のチェックリストから始めてみてください。私がこけたところを避ければ、きっとスムーズに公開まで行けるはずです。もし迷ったら、ぜひ声をかけてください。同じ道を通った者として、できる限りお手伝いします。
最後まで読んでいただき、ありがとうございました。小さなプラグイン記事が、あなたの開発のお役に立てば幸いです。
