0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめて OpenCode プラグインを作って npm 公開してみた。つまずいた箇所、全部書きます

0
Last updated at Posted at 2026-08-29

ChatGPT Image Aug 30, 2026, 03_48_01 AM.png

はじめて OpenCode プラグインを作りました。名前は opencode-autopilot-logbook です。セッションがアイドルになったら、勝手に日報 YYYYMMDD_logbook.md を作ってくれる、ちょっと気が利くプラグインです。

正直に言います。作るのは楽しかったのですが、公開までの道のりで何度もこけました。公式ドキュメントを読んだだけではわからない「地味な落とし穴」がいくつもあったのです。

この記事では、計画書と Git 履歴を振り返りながら、後から続くエンジニアの方が同じところで迷わないよう、具体的なコマンドと失敗例つきで残します。


成果物

今回作成したプラグインの成果物はこちらです。

インストール方法

npm install -g opencode-autopilot-logbook
opencode plugin opencode-autopilot-logbook -g

セットアップ方法

  1. OpenCode を再起動します。環境変数は起動時に読み込まれます
  2. セッションで少し作業して、アイドル状態にします
  3. 既定では 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 で *** に置き換えるのですが、順番を間違えると漏れます。

悪い順序

  1. 長い transcript を 12,000 文字で切り詰める
  2. そのあとマスキング

切り口で sk- が分断されると、正規表現が当たらず 2 文字だけ残ってしまうのです。正しい順序は逆でした。

良い順序

  1. まずマスキング
  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 プラグインを作るなら、この記事のチェックリストから始めてみてください。私がこけたところを避ければ、きっとスムーズに公開まで行けるはずです。もし迷ったら、ぜひ声をかけてください。同じ道を通った者として、できる限りお手伝いします。


最後まで読んでいただき、ありがとうございました。小さなプラグイン記事が、あなたの開発のお役に立てば幸いです。


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?