SLOPSHOPPER

general-dev-mods

開発全般で使える Claude Mods 集。現在は cache-ttl-compact (離席が続いてプロンプトキャッシュの 1 時間 TTL が失効する前に conversation を自動で compact する) を含む。

newtimer
A shopper browsing a rack in a slop shop
README

Claude Code / Codex Marketplace

自分専用の Claude Code / Codex プラグインマーケットプレイス。

セットアップ

1. GitHub にリポジトリを作成・push

cd suntory-n-water-marketplace
git init
git add .
git commit -m "Initial marketplace setup"
gh repo create suntory-n-water-marketplace --public --source=. --push

2. マーケットプレイスを登録

Claude Code 内で以下を実行:

/plugin marketplace add Suntory-N-Water/suntory-n-water-marketplace

3. プラグインをインストール

Claude Code 内で /plugin install を実行(例: ブログ・技術記事の執筆プラグイン):

# ブログ・技術記事の執筆
/plugin install blog-writing@suntory-n-water-marketplace

Codex で利用

Codex 用のリポジトリマーケットプレイスを登録し、利用するプラグインをインストールする。

cd suntory-n-water-marketplace
codex plugin marketplace add .
codex plugin add blog-writing@suntory-n-water-marketplace

他のプラグインを利用する場合は、blog-writing を playwright-best-practices または general-dev-skills に置き換える。 インストール後は新しい Codex セッションを開始する。

プラグインの追加方法

1. プラグインディレクトリを作成

plugins/
└── my-plugin/
    ├── .claude-plugin/
    │   └── plugin.json      # 必須: プラグインメタデータ
    ├── .codex-plugin/
    │   └── plugin.json      # 必須: Codex プラグインメタデータ
    ├── commands/             # スラッシュコマンド (任意)
    │   └── my-command.md
    ├── agents/               # エージェント定義 (任意)
    │   └── my-agent.md
    ├── skills/               # スキル定義 (任意)
    │   ├── my-skill/
    │   │   └── SKILL.md
    │   └── deprecated/       # 使うのをやめたスキルの置き場 (任意)
    │       ├── README.md
    │       └── old-skill/
    ├── hooks/                # フック定義 (任意)
    │   └── hooks.json
    ├── .mcp.json             # MCP サーバー設定 (任意)
    └── README.md

Claude Code 用のマーケットプレイスは .claude-plugin/marketplace.json、Codex 用の リポジトリマーケットプレイスは .agents/plugins/marketplace.json で管理する。 両方の plugin.json から同じ skills/ を参照する。

2. plugin.json を作成

Claude Code 用と Codex 用の両方の plugin.json を作成する。Codex 用では、同梱する スキルの場所とインストール画面用のメタデータを指定する。

{
  "name": "my-plugin",
  "description": "プラグインの説明",
  "version": "1.0.0",
  "author": {
    "name": "作者名"
  }
}

Codex 用の .codex-plugin/plugin.json には、少なくとも skills と interface を追加する。 skills はディレクトリ指定ではなくスキルごとのパスの配列にする。Codex は skills/ 配下を再帰的に探すため、ディレクトリ指定だと skills/deprecated/ まで読み込まれる。

{
  "name": "my-plugin",
  "version": "1.0.0",
  "description": "プラグインの説明",
  "author": { "name": "作者名" },
  "skills": ["./skills/my-skill"],
  "interface": {
    "displayName": "My Plugin",
    "shortDescription": "プラグインの説明",
    "longDescription": "プラグインの詳細な説明",
    "developerName": "作者名",
    "category": "Development",
    "capabilities": ["Read", "Write"],
    "defaultPrompt": ["このプラグインを使って作業して"]
  }
}

3. marketplace.json にエントリを追加

.claude-plugin/marketplace.json の plugins 配列に追加:

{
  "name": "my-plugin",
  "description": "プラグインの説明",
  "version": "1.0.0",
  "author": { "name": "作者名" },
  "source": "./plugins/my-plugin",
  "category": "development"
}

Codex 用は .agents/plugins/marketplace.json の plugins 配列に、 source.path を ./plugins/my-plugin とするエントリを追加する。

4. コミット・push

git add .
git commit -m "Add my-plugin"
git push

スキルの廃止方法

使うのをやめたスキルは削除せず、plugins/<plugin-name>/skills/deprecated/ へ移す。 記述と履歴を残したまま読み込みだけを止められる。

  1. skills/<skill-name>/ を skills/deprecated/<skill-name>/ へ移す
  2. .codex-plugin/plugin.json の skills 配列から該当パスを外す
  3. skills/deprecated/README.md にやめた理由と後継を書く
  4. bun run version-bump <plugin-name> minor でバージョンを上げる

Claude Code は skills/ 直下の 1 階層しか見ないので、移した時点で読み込まれなくなる。 Codex は skills/ 配下を再帰的に探すため、配列から外す手順が要る。

バージョン管理

プラグインのバージョンを semver 形式でバンプするスクリプトを用意している。

bun run version-bump <plugin-name> <major|minor|patch>

例

# パッチ: 1.0.0 -> 1.0.1
bun run version-bump playwright-best-practices patch

# マイナー: 1.0.0 -> 1.1.0
bun run version-bump blog-writing minor

# メジャー: 1.0.0 -> 2.0.0
bun run version-bump general-dev-skills major
Source 1 files
hooks/cache-ttl-compact/register.ts 110 lines
1import type { On, PluginOptions, Timer } from 'claude-code';
2
3const MINUTE = 60_000;
4
5export function register(on: On, options: PluginOptions): void {
6  const idleMs = Number(options.cacheTtlCompactIdleMinutes ?? 55) * MINUTE;
7  const tickMs = Number(options.cacheTtlCompactTickMinutes ?? 1) * MINUTE;
8
9  let timer: Timer | undefined;
10  let lastTurnEndedAt = 0;
11  let done = false;
12
13  on('session.start', async ($, e, next) => {
14    // $.clock.now() は host を通るので、claude-code/testing の mock.clock で
15    // 差し替えられる。Date.now() だと 1 時間待たないとテストできない。
16    lastTurnEndedAt = await $.clock.now();
17    $.ui.log(
18      `cache-ttl-compact: ${idleMs / MINUTE} 分の無操作で compact します ` +
19        `(確認の間隔は ${tickMs / MINUTE} 分)`,
20    );
21
22    timer?.cancel();
23    timer = $.clock.every(tickMs, async () => {
24      if (done) {
25        return;
26      }
27
28      const elapsed = (await $.clock.now()) - lastTurnEndedAt;
29      if (elapsed < idleMs) {
30        return;
31      }
32
33      done = true;
34      try {
35        const result = await $.session.compact();
36        // skip は boolean ではなく理由の文字列。空文字も string に含まれるため、
37        // if (result.skip) では compact 済みの型まで絞り込めない。
38        if (result.skip !== undefined) {
39          $.ui.log(
40            `cache-ttl-compact: 他の Hook が compact を拒否しました (${result.skip})`,
41          );
42          return;
43        }
44        $.ui.log(
45          `cache-ttl-compact: ${Math.round(elapsed / MINUTE)} 分の離席を検知し、` +
46            `${result.tokensBefore ?? '?'} → ${result.tokensAfter ?? '?'} トークンに compact しました`,
47        );
48      } catch (error) {
49        // $.session.compact() はターンの実行中だと reject される。
50        // タイマーの周期とターンの開始がぶつかった場合なので、次の周期に回す。
51        done = false;
52        $.ui.log(`cache-ttl-compact: compact できませんでした (${error})`, {
53          to: 'debug',
54        });
55      }
56    });
57
58    return next(e);
59  });
60
61  // この mod 以外の compact (/compact や閾値での自動 compact) の後も、
62  // 次のターンが始まるまでは compact 済みの会話なので compact し直さない。
63  on('session.compact', async (_$, e, next) => {
64    const result = await next(e);
65    if (
66      e.agentId === undefined &&
67      e.trigger !== 'precompute' &&
68      result.skip === undefined
69    ) {
70      done = true;
71    }
72    return result;
73  });
74
75  // done を戻すのは turn.complete ではなく turn.start。ターンの途中で
76  // 自動 compact が実行された場合、turn.complete で戻すと compact 済みの印が消える。
77  // turn.start はサブエージェントでは発火しないので agentId を見なくてよい。
78  on('turn.start', (_$, e, next) => {
79    done = false;
80    return next(e);
81  });
82
83  on('turn.complete', async ($, e, next) => {
84    // agentId があるのはサブエージェントのターン。サブエージェントのリクエストは
85    // メインの conversation のキャッシュを読まないので、TTL の起点にならない。
86    if (e.agentId !== undefined) {
87      return next(e);
88    }
89
90    lastTurnEndedAt = await $.clock.now();
91    return next(e);
92  });
93
94  on('session.end', async ($, e, next) => {
95    // /clear・/resume・/branch でも session.end は発火するが、プロセスは終了せず
96    // session.start も再発火しない。ここでタイマーを止めると再開されないので、
97    // 起点だけ新しい会話に合わせ直す。/branch の reason は resume になる。
98    if (e.reason === 'clear' || e.reason === 'resume') {
99      lastTurnEndedAt = await $.clock.now();
100      // /clear 直後の会話は空なので、次のターンが始まるまで compact しない。
101      done = e.reason === 'clear';
102      return next(e);
103    }
104
105    timer?.cancel();
106    timer = undefined;
107    return next(e);
108  });
109}
110