@interactive-inc/claude-funnel 0.36.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,97 +1,53 @@
1
1
  [![npm](https://img.shields.io/npm/v/@interactive-inc/claude-funnel.svg)](https://www.npmjs.com/package/@interactive-inc/claude-funnel)
2
2
  [![license](https://img.shields.io/npm/l/@interactive-inc/claude-funnel.svg)](./LICENSE)
3
3
 
4
- A hub for AI coding agents. One long-running daemon owns all external connections; agents subscribe to named channels and react to events without you wiring up shell scripts and cron entries. Outbound replies travel back through the same connectors as MCP tools, so answering a message or commenting on an issue does not need a bash subshell.
4
+ # Open Claude Funnel
5
5
 
6
- The command is `funnel` (or the shorthand `fnl`).
7
-
8
- Connectors today: Slack (Socket Mode), GitHub (poll via `gh`), Discord (Gateway), and cron schedules. Built around Claude Code; the architecture is agent-agnostic.
9
-
10
- ## Why funnel
11
-
12
- A single agent session is great at one repository at one moment. The moment you want it to react to things — a chat mention, a new issue, a 9 AM standup — you end up gluing shell scripts, cron entries, and `bash -c "agent ..."` invocations together. There is no single place that says "who is listening to what, and who is allowed to reply where."
13
-
14
- funnel is that place. Declare named subscription boxes (channels), attach connectors to them, launch the agent with a channel binding, and the daemon handles the rest:
15
-
16
- - The daemon owns the external connections. Each one connects once, no matter how many agent sessions you start. A second agent does not open a second socket; both sessions subscribe to the same channel and the daemon fans events out.
17
- - Inbound events arrive as MCP notifications, so the agent reacts in the session it is already running in.
18
- - Outbound replies use MCP tools per connector — essentially synchronous (no bash, no CLI cold start).
19
- - Listeners are supervised with health checks and automatic restart; a flaky connection or crashed poller recovers on its own.
20
- - Multiple agents can share a channel (`fanout`) or compete for events as workers (`exclusive`) — the daemon decides who gets each event.
21
-
22
- ## Concepts
6
+ Slack のメンション、GitHub Issue、毎朝 9 時の cron。こうした外部の出来事を Claude Code エージェントに届け、エージェントの返信を同じ経路で外へ返すハブ。
23
7
 
24
8
  ```
25
- external sources outbound replies
26
- (chat / source-control / cron) (MCP tools per connector)
27
- │ │
28
- ▼ ▼
29
- daemon (port 9742)
30
- routes events into channels
31
- serves replies through the same connectors
32
-
33
- ▼ WebSocket / MCP (stdio)
34
- agent (subscribes to one channel)
9
+ Slack ─┐ ┌─→ Claude エージェント A
10
+ GitHub ─┤ │
11
+ Discord ─┼──→ funnel daemon ──→┼─→ Claude エージェント B
12
+ cron ─┘ │
13
+ └─→ Claude エージェント C
14
+
15
+ funnel daemon が外部接続を1か所に常駐保持し、購読中の各エージェントへ配信する。
16
+ 返信は同じコネクタを逆向きに通る(エージェント → MCP tool → 外部サービス)。
35
17
  ```
36
18
 
37
- Two concepts make up the transport model:
38
-
39
- Channel — a named subscription box (transport only). Holds one or more connectors and a delivery mode; it does not carry launch flags. An agent session subscribes to exactly one channel. Delivery is `fanout` (every subscriber sees every event, the default) or `exclusive` (one event per subscriber, round-robin — for worker pools).
40
-
41
- Connector — a single attachment from a channel to an external source. Four types ship today: `slack`, `gh`, `discord`, `schedule`. The first three are bidirectional (events in, replies out); `schedule` is one-way (cron ticks in).
19
+ コマンドは `funnel`(短縮形 `fnl`)。Claude Code を中心に作っているが、アーキテクチャはエージェント非依存。
42
20
 
43
- Profile sits on top of that model as a launch convenience, not part of it — you never need one to run an agent (`fnl claude --channel <name>` is enough). It is a saved launch preset bundling `{ path, channelId, options, env, resume }` so `fnl claude --profile cto` reproduces a known setup: which directory to launch from, which channel to bind, and the launch recipe (args prepended to the claude argv, env layered under the process, session reuse). A profile carries a stable uuid `id` (the unit the PID file and the resumable session key off, so renaming it strands neither); `name` is just the handle you type. The first profile in the list is the default. Because a profile already binds a channel, `--profile` and `--channel` cannot be combined.
21
+ ## funnel がやること
44
22
 
45
- The daemon is where all external connections live. It runs on port 9743 for `funnel` CLI launches (9742 for a gateway hosted programmatically, so the two never collide on one machine — `FUNNEL_PORT` overrides either), bound to loopback (`127.0.0.1`) only so it is never reachable off-box; set `FUNNEL_HOST=0.0.0.0` to expose it deliberately (every privileged endpoint still requires the bearer token regardless). It supervises connectors with auto-restart, broadcasts events to subscribed agent sessions over WebSocket, and serves the reply API that MCP calls. Starting or stopping an agent never starts or stops external connections.
23
+ - 外部の出来事(チャットのメンション、新しい Issue、cron tick)を、起動中のエージェントセッションにそのまま届ける
24
+ - エージェントの返信を受信と同じコネクタで外へ返す。bash サブシェルも CLI のコールドスタートもなく、実質同期
25
+ - 外部接続は常駐デーモンが 1 か所で持つ。エージェントを何個起こしても接続は 1 回だけ。ヘルスチェックと自動再起動で監視する
26
+ - 複数のエージェントで 1 つのソースを共有(fanout)するか、ワーカーとして分担(exclusive)するかを選べる
46
27
 
47
- The MCP layer is a thin bridge into the agent. It subscribes to the bound channel over WebSocket (the daemon does the work) and exposes one tool per callable connector so the agent can reply back out.
28
+ 対応コネクタは Slack(Socket Mode)、GitHub(`gh` 経由の poll)、Discord(Gateway)、cron スケジュールの 4 種類。
48
29
 
49
- ## Requirements
30
+ ## 必要なもの
50
31
 
51
- - [Bun](https://bun.sh) 1.3 or later
32
+ - [Bun](https://bun.sh) 1.3 以降
52
33
  - [Claude Code](https://docs.claude.com/en/docs/claude-code) CLI
53
- - A token or CLI for whichever external service you connect (Slack app, `gh` auth, Discord bot, etc.)
34
+ - 接続する外部サービスのトークンまたは CLISlack アプリ、`gh` 認証、Discord bot など)
54
35
 
55
- ## Install
36
+ ## リポジトリで使う(推奨)
56
37
 
57
- Per-repo (recommended for getting started — no global install, version pinned by the repo's lock file):
38
+ 設定を `funnel.json` としてリポジトリに commit し、チームで共有・バージョン管理できる。グローバルに何も入れないので、リポジトリの lock ファイルでバージョンが固定される。
58
39
 
59
- ```bash
60
- bun add -D @interactive-inc/claude-funnel
61
- bunx funnel claude # or `bunx funnel <any subcommand>`
62
- ```
63
-
64
- Global (one CLI for every repo you touch):
40
+ ### インストール
65
41
 
66
42
  ```bash
67
- bun add -g @interactive-inc/claude-funnel
68
- funnel claude # or the `fnl` shorthand
43
+ bun add -D @interactive-inc/claude-funnel
69
44
  ```
70
45
 
71
- The published package ships the built `dist/`, so either install makes `funnel` / `fnl` available immediately — no post-install step. The rest of the README uses `fnl` for brevity; swap in `bunx funnel` if you went the per-repo route.
46
+ これで `bunx funnel`(または `bunx fnl`)が使える。
72
47
 
73
- ## Quick start
48
+ ### 設定
74
49
 
75
- Wire one source to one agent:
76
-
77
- ```bash
78
- fnl channels add ops
79
- fnl channels ops connectors add my-slack --type=slack \
80
- --bot-token=xoxb-... --app-token=xapp-...
81
- fnl gateway start
82
- fnl claude --channel ops
83
- ```
84
-
85
- Every event the connector sees now arrives in the running agent session, and the agent can reply via the `my-slack` MCP tool.
86
-
87
- Save it as a profile for one-command launches — the profile carries the launch recipe:
88
-
89
- ```bash
90
- fnl profiles add cto --path=/repo/myapp --channel=ops --agent=pm --options="--brief"
91
- fnl claude --profile cto # cd + channel binding + recipe in one shot
92
- ```
93
-
94
- Or drop a `funnel.json` in the repo and `fnl claude` (no args) inside the repo will use it:
50
+ リポジトリ直下に `funnel.json` を置く。transport(`channels[]`)と起動レシピ(`profiles[]`)を宣言する。
95
51
 
96
52
  ```json
97
53
  {
@@ -99,12 +55,7 @@ Or drop a `funnel.json` in the repo and `fnl claude` (no args) inside the repo w
99
55
  "channels": [
100
56
  {
101
57
  "name": "ops",
102
- "connectors": [
103
- {
104
- "type": "slack",
105
- "name": "my-slack"
106
- }
107
- ]
58
+ "connectors": [{ "type": "slack", "name": "my-slack" }]
108
59
  },
109
60
  {
110
61
  "name": "review"
@@ -112,11 +63,14 @@ Or drop a `funnel.json` in the repo and `fnl claude` (no args) inside the repo w
112
63
  ],
113
64
  "profiles": [
114
65
  {
66
+ "name": "pm",
115
67
  "channel": "ops",
116
68
  "options": ["--brief", "--agent", "pm"],
117
- "env": { "ANTHROPIC_MODEL": "claude-sonnet-4-6" }
69
+ "env": { "ANTHROPIC_MODEL": "claude-sonnet-4-6" },
70
+ "resume": true
118
71
  },
119
72
  {
73
+ "name": "reviewer",
120
74
  "channel": "review",
121
75
  "options": ["--agent", "reviewer"]
122
76
  }
@@ -124,114 +78,165 @@ Or drop a `funnel.json` in the repo and `fnl claude` (no args) inside the repo w
124
78
  }
125
79
  ```
126
80
 
127
- `channels[]` is required and the first entry is the default. `fnl claude --channel review` picks one by name; `fnl claude` with no `--channel` uses the first.
81
+ channels は購読箱(transport)。各チャネルは `connectors` を持てる。`connectors` 配列はそのチャネルの真実の源で、起動時に宣言にないコネクタは削除され、足りないコネクタは作られる(名前で照合)。`connectors` フィールド自体が無ければ既存のコネクタはそのまま残る。
82
+
83
+ profiles は起動レシピ。各プロファイルは一意な `name` を持ち、`channel` でチャネルを名前指定でバインドし、次を持つ。
128
84
 
129
- A channel declares only transport — its `connectors` and delivery mode. The launch recipe lives on `profiles[]`: each profile has a unique `name`, binds to a channel by name, and carries `options` (prepended to the claude argv before user-supplied CLI args, which still come last — use it for flags like `--brief`, `--agent <name>`, `--model <name>`), `env` (layered under the launched claude process — `process.env` from the launching shell wins on collision), and `resume`. A profile is launched by name with `fnl claude --profile <name>`; the channel never selects a profile on its own. Multiple profiles may bind the same channel — they are told apart by `name`.
85
+ - `options` claude argv 先頭に積む(ユーザーが渡す CLI 引数はその後ろ。`--brief` / `--agent <name>` / `--model <name>` などに使う)
86
+ - `env` — 起動する claude プロセスに被せる(衝突時は起動シェルの `process.env` が勝つ)
87
+ - `resume` — claude セッションの再利用可否
130
88
 
131
- The optional `connectors` array on a channel is the source of truth for that channel: missing connectors are created and connectors not declared are removed on launch. Connectors are matched by name. An absent `connectors` field leaves existing connectors alone.
89
+ 同じチャネルに複数のプロファイルをバインドしてよく、`name` で区別する。チャネル側がプロファイルを選ぶことはない(プロファイルがチャネルを bind する一方向)。
132
90
 
133
- A repo with a `funnel.json` is scoped to itself. On first launch funnel writes a stable `id` (uuid) to the top of `funnel.json` and keeps every byte of funnel state under `~/.funnel/projects/<id>/`, never touching the global `~/.funnel` — only the event log and temp files under `/tmp/funnel/` are shared. This scoping applies to every CLI command run in the repo, not just `fnl claude`. On launch the chosen channel's transport (`connectors`, delivery) is materialized into `~/.funnel/projects/<id>/settings.json`; the profile recipe is passed straight to the launcher and is not persisted there. Raw launches (`fnl claude --channel <name>` without funnel.json) bind transport only against the global `~/.funnel` and carry no recipe.
91
+ トークンは `funnel.json` に書かない(スキーマが弾く)。リポジトリ内で次のどちらか。
134
92
 
135
- The optional top-level `$schema` points at the JSON Schema so editors can validate and autocomplete the file. The recommended reference for repos with a local install is `./node_modules/@interactive-inc/claude-funnel/funnel.schema.json` — it works without a network round-trip and editors do not need to prompt for trust. The same file is also published at `https://interactive-inc.github.io/open-claude-funnel/funnel.schema.json` (editors usually require explicit trust on first use), and `fnl schema > funnel.schema.json` regenerates a local copy on demand.
93
+ - `fnl channels ops connectors set my-slack --bot-token=xoxb-... --app-token=xapp-...` で設定する
94
+ - 省略して `fnl claude` 起動時の TTY プロンプトで答える。次回以降は引き継がれ再度聞かれない
136
95
 
137
- Connectors in `funnel.json` carry no tokens. A connector's token is set out of band and stored only in the repo-scoped settings:
96
+ いずれもトークンは `~/.funnel/projects/<id>/settings.json`(リポジトリ外)に保存され、commit されない。
138
97
 
139
- - run `fnl channels <ch> connectors set <c> --bot-token=...` (etc.) in the repo to write it to `~/.funnel/projects/<id>/settings.json`
140
- - or omit it — `fnl claude` prompts on a TTY at launch and saves the answer there; it carries over so later launches do not re-prompt. On non-TTY stdin the launch fails so CI / agent-spawned-agent runs do not hang
98
+ `funnel.json` に書けないものが 2 つある。delivery モード(fanout / exclusive)は CLI で設定する(`funnel.json` のチャネルは fanout 固定)。schedule cron エントリも CLI で足す(`funnel.json` には schedule コネクタの存在だけ宣言できる)。
141
99
 
142
- Only the `id` is ever written back to `funnel.json`; tokens never live in the repo.
100
+ セッション再開には `resume` の明示が要る。`funnel.json``resume` はデフォルト値を持たず、書かないと再開されない(未指定が false 扱いになる)。前回の claude セッションを引き継ぎたいなら必ず `"resume": true` を書く。再開のための session id funnel が `~/.funnel/projects/<id>/settings.json` に自動で保存・読み出しするので、`funnel.json` には書かない。
143
101
 
144
- Cron-driven agent runs:
102
+ ### 使い方
145
103
 
146
104
  ```bash
147
- fnl channels ops connectors add daily --type=schedule
148
- fnl channels ops connectors daily schedules add morning \
105
+ bunx fnl gateway start # 常駐デーモンを起動(外部接続を持つ)
106
+ bunx fnl claude --profile pm # cd + チャネルのバインド + レシピを一発で
107
+ ```
108
+
109
+ `--profile` なしの `bunx fnl claude` は、`funnel.json` の先頭チャネルで起動する。`--channel review` で名前指定すると、そのチャネルを transport だけバインドして起動する(レシピなし)。
110
+
111
+ これでコネクタが見たイベントが、起動中のエージェントセッションに届く。エージェントは `my-slack` という MCP ツールで返信できる。
112
+
113
+ cron 起動を足す(schedule コネクタを宣言したうえで、エントリは CLI で)。
114
+
115
+ ```bash
116
+ bunx fnl channels ops connectors add daily --type=schedule
117
+ bunx fnl channels ops connectors daily schedules add morning \
149
118
  --cron="0 9 * * *" --prompt="morning standup"
150
119
  ```
151
120
 
152
- Each tick fires the prompt into the channel. If the daemon was down at 9 AM, the next start catches up the missed slot (`meta.catchup = "true"`) for up to 24 hours.
121
+ tick ごとにプロンプトがチャネルへ発火する。9 時にデーモンが落ちていても、次回起動時に逃した枠を catch-up する(`meta.catchup = "true"`、最大 24 時間)。
122
+
123
+ `funnel.json` を持つリポジトリは自分自身にスコープされる。初回起動時に funnel は `funnel.json` の先頭へ不変の `id`(uuid) を書き戻し、以降このリポジトリの funnel state を `~/.funnel/projects/<id>/` 配下に隔離する。グローバルの `~/.funnel` には一切触らない(イベントログと一時ファイルだけが `/tmp/funnel/` で共有)。このスコープはリポジトリ内で実行する全 CLI コマンドに効く。
124
+
125
+ トップレベルの `$schema`(任意)は JSON Schema を指し、エディタの検証と補完が効く。ローカルインストールでは `./node_modules/@interactive-inc/claude-funnel/funnel.schema.json` を推奨する(ネットワーク往復なし)。`https://interactive-inc.github.io/open-claude-funnel/funnel.schema.json` でも公開しており、`fnl schema > funnel.schema.json` でローカルコピーを再生成できる。
153
126
 
154
- Multiple agents on the same source — pick the delivery mode:
127
+ ## グローバルで使う
128
+
129
+ 触るすべてのリポジトリで 1 つの CLI を共有したいときはグローバルに入れる。設定は `funnel.json` ではなく CLI で組み、`~/.funnel/settings.json`(グローバル)に保存される。
155
130
 
156
131
  ```bash
157
- fnl channels add reviews # fanout (default): every agent sees every event
158
- fnl channels add ingest --delivery=exclusive # exclusive: one event per agent, round-robin
132
+ bun add -g @interactive-inc/claude-funnel
133
+ ```
134
+
135
+ これで `funnel` / `fnl` がどこでも使える。ソース 1 つをエージェント 1 つにつなぐ最短手順。
136
+
137
+ ```bash
138
+ fnl channels add ops
139
+ fnl channels ops connectors add my-slack --type=slack \
140
+ --bot-token=xoxb-... --app-token=xapp-...
141
+ fnl gateway start
142
+ fnl claude --channel ops
143
+ ```
144
+
145
+ delivery モードはチャネル作成時に選ぶ。
146
+
147
+ ```bash
148
+ fnl channels add reviews # fanout(デフォルト): 全エージェントが全イベント
149
+ fnl channels add ingest --delivery=exclusive # exclusive: 1 イベントを 1 エージェントが round-robin
150
+ ```
151
+
152
+ ワンコマンド起動のためにプロファイルとして保存する。プロファイルは起動レシピを持つ。
153
+
154
+ ```bash
155
+ fnl profiles add cto --path=/repo/myapp --channel=ops --agent=pm --options="--brief"
156
+ fnl claude --profile cto
159
157
  ```
160
158
 
161
- ## CLI surface
162
-
163
- Connectors live nested inside their owning channel. Every write verb (`add` / `set` / `remove` / `rename` / `as-default` / `request`) maps to `POST` plus the verb in the URL — the same word stays visible in shell and HTTP form. Read paths stay `GET`.
164
-
165
- ```text
166
- fnl channels list
167
- fnl channels add <name> [--delivery=fanout|exclusive]
168
- fnl channels <name> show details
169
- fnl channels remove <name>
170
- fnl channels rename <old> <new> (also `fnl channels <old> rename <new>`)
171
- fnl channels <name> set delivery <mode>
172
-
173
- fnl channels <ch> connectors list
174
- fnl channels <ch> connectors add <c> --type=slack --bot-token=xoxb-... --app-token=xapp-...
175
- fnl channels <ch> connectors add <c> --type=gh [--poll-interval=<sec>]
176
- fnl channels <ch> connectors add <c> --type=discord --bot-token=<token>
177
- fnl channels <ch> connectors add <c> --type=schedule
178
- fnl channels <ch> connectors <c> show config
179
- fnl channels <ch> connectors set <c> [--bot-token=...] [--app-token=...] [--poll-interval=...]
180
- fnl channels <ch> connectors remove <c>
181
- fnl channels <ch> connectors rename <c> <new>
182
- fnl channels <ch> connectors <c> request --method=<api.method> [--key=value ...]
183
-
184
- fnl channels <ch> connectors <c> schedules list cron entries
185
- fnl channels <ch> connectors <c> schedules add <id> --cron="<expr>" --prompt="<text>" \
186
- [--enabled=true] [--catchup-policy=latest|all|skip]
187
- fnl channels <ch> connectors <c> schedules remove <id>
188
-
189
- fnl profiles list (first entry is the default)
190
- fnl profiles add <name> --path=<dir> --channel=<channel-name> \
191
- [--agent=<name>] [--options="<argv>"] [--env="K=V,K2=V2"] [--no-resume]
192
- fnl profiles <name> launch (alias for `<name> run`)
193
- fnl profiles <name> run launch (sugar for `fnl claude --profile <name>`)
194
- fnl profiles <name> set [--path=...] [--channel=...] \
195
- [--agent=...] [--options="..."] [--env="..."] [--resume|--no-resume]
196
- fnl profiles <name> as-default move to the front of the list
197
- fnl profiles rename <old> <new>
198
- fnl profiles remove <name>
199
-
200
- fnl claude launch the first channel from ./funnel.json, or the default profile
201
- fnl claude --channel <name> with funnel.json: pick that channel; without: raw launch
202
- fnl claude --profile <name> launch a named profile (ignores funnel.json; cannot combine with --channel)
203
- fnl claude [...] positionals and any flag other than -p / --profile / --channel
204
- (e.g. --agent, --resume, -c, --model) pass through to claude
205
- fnl mcp run as an MCP server (invoked from .mcp.json)
206
-
207
- fnl gateway status (default subcommand)
208
- fnl gateway {start|stop|restart} daemon lifecycle
209
- fnl gateway run foreground daemon (developer mode)
210
- fnl gateway logs [-n <N>] tail diagnostic log
211
- fnl gateway listeners live registry (alive / dead)
212
-
213
- fnl status overall status (channels / profiles / gateway / clients)
214
- fnl schema print the JSON Schema for funnel.json (pipe to a file for editor support)
215
- fnl update `bun i -g @interactive-inc/claude-funnel`
216
- fnl (no args) show help
217
-
218
- fnl --version
219
- fnl --help every subcommand has --help; verb-without-arg also returns help
159
+ 公開パッケージはビルド済みの `dist/` を同梱しているので、どちらのインストールでも `funnel` / `fnl` がすぐ使える(post-install ステップなし)。リポジトリ単位で入れたなら、以降の `fnl` は `bunx funnel` に読み替える。
160
+
161
+ コマンドの一覧やフラグはここに並べない。`fnl --help` で全体、`fnl <command> --help`(例 `fnl channels --help`)で個別の使い方が出る。動詞だけを引数なしで打ってもヘルプが返る。
162
+
163
+ ## なぜ funnel なのか
164
+
165
+ 1 つのエージェントセッションは、1 つのリポジトリ・1 つの瞬間を扱うのは得意だ。だがそれに「何かに反応させたい」と思った瞬間 — チャットのメンション、新しい Issue、朝 9 時のスタンドアップ — シェルスクリプトと cron と `bash -c "agent ..."` をつなぎ合わせる羽目になる。「誰が何を聞いていて、誰がどこに返信していいのか」を一望できる場所がどこにもない。
166
+
167
+ funnel はその場所になる。名前付きの購読箱(チャネル)を宣言し、コネクタを取り付け、チャネルをバインドしてエージェントを起動すれば、あとはデーモンが面倒を見る。使うモチベーションは次の点にある。
168
+
169
+ 外部接続をデーモンが 1 か所で持つ。各接続は、エージェントセッションを何個起こしても 1 回だけつながる。2 つ目のエージェントが 2 つ目のソケットを開くことはなく、両方が同じチャネルを購読し、デーモンがイベントを fanout する。
170
+
171
+ 受信イベントは MCP 通知として届くので、エージェントは今いるセッションのまま反応する。新しいプロセスを起こさない。
172
+
173
+ 送信(返信)はコネクタごとの MCP ツールを使う。bash CLI のコールドスタートもなく、実質同期で返る。
174
+
175
+ リスナーはヘルスチェックと自動再起動で監視される。接続が不安定でも poller がクラッシュしても、自分で復旧する。
176
+
177
+ 複数のエージェントが 1 つのチャネルを共有する(fanout)か、ワーカーとしてイベントを取り合う(exclusive)かを選べる。どのイベントを誰が受けるかはデーモンが決める。
178
+
179
+ つまり、外部とのつなぎ込みという「配管」を 1 つのデーモンに集約し、エージェント側は購読と返信という単純な世界だけを見ればよくなる。これが funnel を使う理由になる。
180
+
181
+ ## 仕組み
182
+
183
+ ### 全体像
184
+
185
+ ```
186
+ external sources outbound replies
187
+ (chat / source-control / cron) (MCP tools per connector)
188
+ │ │
189
+ ▼ ▼
190
+ daemon (port 9743 for CLI)
191
+ routes events into channels
192
+ serves replies through the same connectors
193
+
194
+ ▼ WebSocket / MCP (stdio)
195
+ agent (subscribes to one channel)
220
196
  ```
221
197
 
222
- `--channel` accepts the channel name (not the uuid). The CLI resolves it to a channel id before calling the engine.
198
+ ### Channel Connector Profile
199
+
200
+ transport モデルは 2 つの概念でできている。
201
+
202
+ Channel は名前付きの購読箱(transport のみ)。1 つ以上のコネクタと delivery モードを持ち、起動フラグは持たない。エージェントセッションはちょうど 1 つのチャネルを購読する。delivery は `fanout`(全 subscriber が全イベントを見る、デフォルト)か `exclusive`(1 イベントを 1 subscriber が round-robin で消費、ワーカープール向け)。
203
+
204
+ Connector はチャネルから外部ソースへの 1 つの接続。`slack` / `gh` / `discord` / `schedule` の 4 型。前者 3 つは双方向(イベント入力・返信出力)、`schedule` は一方向(cron tick の入力のみ)。
223
205
 
224
- ## Outbound calls (MCP tools per connector)
206
+ Profile はその transport モデルの外側にある起動の便宜レイヤで、モデルの一部ではない。エージェントを動かすのに必須ではない(`fnl claude --channel <name>` で足りる)。`{ path, channelId, options, env, resume }` を束ねた保存済みの起動 preset で、`fnl claude --profile cto` が既知のセットアップを再現する。どのディレクトリから起動するか、どのチャネルをバインドするか、起動レシピ(claude argv の先頭に積む引数、プロセスに被せる env、セッション再利用)をまとめて持つ。プロファイルは不変の uuid `id` を持ち(PID ファイルと再開可能なセッションがこれをキーにするので、rename してもどちらも迷子にならない)、`name` は人が打つハンドル。プロファイルは既にチャネルをバインドしているので、`--profile` と `--channel` は併用できない。
225
207
 
226
- When `fnl claude` launches the agent, the funnel MCP server connects to the daemon and reads the channel's connectors from `~/.funnel/settings.json`. For every callable connector (`slack` / `discord` / `gh`; `schedule` is one-way and skipped), MCP advertises one tool with the connector's name. The agent calls them like:
208
+ ### daemon
209
+
210
+ 外部接続はすべてデーモンに住む。`funnel` CLI 起動では port 9743 で動く(プログラムから起こす gateway は 9742 なので、1 マシン上で衝突しない。`FUNNEL_PORT` でどちらも上書き可)。bind は loopback(`127.0.0.1`)のみで off-box から到達不可。`FUNNEL_HOST=0.0.0.0` で意図的に公開できる(公開しても全特権エンドポイントは bearer token 必須)。デーモンはコネクタを自動再起動付きで監視し、イベントを WebSocket で購読中のエージェントセッションへ broadcast し、MCP が呼ぶ返信 API を提供する。エージェントの起動・停止が外部接続を起動・停止することはない。
211
+
212
+ ### MCP
213
+
214
+ MCP レイヤはエージェントへの薄いブリッジ。バインドされたチャネルを WebSocket で購読し(実作業はデーモンがやる)、呼び出し可能なコネクタごとに 1 つのツールを公開して、エージェントが外へ返信できるようにする。
215
+
216
+ ### イベントの旅
217
+
218
+ 1 つの Slack メッセージがエージェントに届くまで。
219
+
220
+ ```
221
+ Slack → SlackListener.start(notify) → notify(channel, connector, content, meta)
222
+ → GatewayServer.notify → Broadcaster.broadcast → event store に seq 付き保存
223
+ → 該当 Channel を購読している WS クライアントに fanout
224
+ → エージェント側 MCP(channel-server)が受信してエージェントに events として渡す
225
+ ```
226
+
227
+ 逆方向(エージェント → Slack)は MCP のコネクタごとの tool 経由。Listener と Adapter は独立した一方向の通路で、Broadcaster は経由しない。
228
+
229
+ ### 外部への送信
230
+
231
+ `fnl claude` がエージェントを起動すると、funnel の MCP サーバがデーモンに接続し、チャネルのコネクタを `~/.funnel/settings.json` から読む。呼び出し可能なコネクタ(`slack` / `discord` / `gh`。`schedule` は一方向なので除外)ごとに、コネクタ名のツールを 1 つ公開する。エージェントはこう呼ぶ。
227
232
 
228
233
  ```jsonc
229
- // MCP: tools/list returns
234
+ // MCP: tools/list が返す
230
235
  { "name": "discord", "inputSchema": { ... { method, path, body } ... } }
231
236
  { "name": "ops-slack", "inputSchema": { ... } }
232
237
  { "name": "gh-main", "inputSchema": { ... } }
233
238
 
234
- // agent calls
239
+ // エージェントの呼び出し
235
240
  tools/call name="discord" arguments={
236
241
  "method": "POST",
237
242
  "path": "/channels/123/messages",
@@ -239,243 +244,160 @@ tools/call name="discord" arguments={
239
244
  }
240
245
  ```
241
246
 
242
- MCP forwards via HTTP `POST /channels/<channel>/connectors/<connector>/call` to the daemon, which dispatches through the connector's adapter. No bash subshell, no CLI cold start — replies are essentially synchronous.
247
+ MCP HTTP `POST /channels/<channel>/connectors/<connector>/call` でデーモンへ転送し、デーモンがコネクタの adapter 経由でディスパッチする。bash サブシェルもコールドスタートもなく、返信は実質同期。
243
248
 
244
- To invoke a connector from outside an agent, the same path is reachable as `fnl channels <ch> connectors <c> request --method=<...> [--key=value ...]`.
249
+ エージェントの外からコネクタを呼ぶには、同じパスを `fnl channels <ch> connectors <c> request --method=<...> [--key=value ...]` で叩ける。
245
250
 
246
- ## Data model
251
+ ### データモデル
247
252
 
248
253
  ```
249
254
  Channel = { id, name, delivery, connectors[] }
250
- subscription box (transport only). delivery is `fanout` (every WS client sees every event)
251
- or `exclusive` (round-robin one client per event). carries no launch settings.
255
+ 購読箱(transport のみ)。delivery `fanout`(全 WS クライアントが全イベント受信)
256
+ `exclusive`(round-robin 1 イベント 1 クライアント)。起動設定は持たない。
252
257
 
253
258
  Connector =
254
259
  | { type: "slack", name, botToken, appToken } Slack Socket Mode
255
- | { type: "gh", name, pollInterval? } GitHub (gh CLI, poll-based)
260
+ | { type: "gh", name, pollInterval? } GitHubgh CLI, poll ベース)
256
261
  | { type: "discord", name, botToken } Discord Gateway
257
- | { type: "schedule", name, entries[] } cron-driven; entries = { id, cron, prompt, enabled?, catchupPolicy? }
262
+ | { type: "schedule", name, entries[] } cron 起動。entries = { id, cron, prompt, enabled?, catchupPolicy? }
258
263
 
259
264
  Profile = { id, name, path, channelId, options[], env, resume, sessionId? }
260
- named launch preset: where to launch (path), which channel to bind, and the launch recipe —
261
- options[] prepends to the claude argv, env layers under the process (process.env wins on
262
- collision), resume toggles session reuse. the first profile is the default. id is a stable
263
- uuid (the key the PID file and resumable session hang off, so a rename strands neither);
264
- name is the CLI handle. sessionId is execution state, not config the claude session this
265
- profile last launched, written by the launcher and read back on the next resume.
265
+ 名前付き起動 preset: どこで起動するか(path)、どのチャネルをバインドするか、起動レシピ。
266
+ options[] claude argv の先頭に積み、env はプロセスに被せ(衝突時は process.env が勝つ)、
267
+ resume はセッション再利用を切り替える。先頭がデフォルト。id は不変の uuid(PID ファイルと
268
+ 再開可能セッションがぶら下がるキー。rename でどちらも迷子にしない)、name CLI のハンドル。
269
+ sessionId config ではなく実行状態で、このプロファイルが最後に起動した claude セッション。
270
+ launcher が書き、次回 resume で読み戻す。
266
271
 
267
272
  LocalConfig = { id?, channels: ChannelSpec[], profiles?: ProfileSpec[] }
268
- per-repo file (funnel.json). channels[] required; first entry is default, --channel selects.
269
- id (uuid) is written back on first launch; this repo's funnel state lives under
270
- ~/.funnel/projects/<id>/ and the global ~/.funnel is never touched.
273
+ リポジトリ単位のファイル(funnel.json)。channels[] 必須、先頭がデフォルト、--channel で選ぶ。
274
+ iduuid)は初回起動時に書き戻され、このリポジトリの funnel state
275
+ ~/.funnel/projects/<id>/ 配下に住み、グローバルの ~/.funnel には一切触れない。
271
276
 
272
277
  ChannelSpec = { name, connectors? }
273
- transport declaration (no id, since funnel.json declares by name). connectors materialize into
274
- the matching Channel in ~/.funnel/projects/<id>/settings.json on launch. Connectors carry no
275
- tokens; a token is set via the CLI or prompted on a TTY at launch and saved to that scoped settings.
278
+ transport 宣言(funnel.json は名前で宣言するので id はない)。connectors は起動時に
279
+ ~/.funnel/projects/<id>/settings.json の該当 Channel materialize する。コネクタはトークンを
280
+ 持たず、CLI TTY 起動時のプロンプトで設定し、そのスコープ settings に保存される。
276
281
 
277
282
  ProfileSpec = { name, channel, options?, env?, resume? }
278
- launch recipe bound to a channel by name. applied inline on launch (the first spec bound to the
279
- chosen channel — selected by its channel binding, not by name); not persisted into the global
280
- profiles[] list.
283
+ チャネルに名前でバインドする起動レシピ。`fnl claude --profile <name>` name 解決して適用され、
284
+ グローバルの profiles[] リストには永続化されない。
281
285
 
282
- Settings = { channels[], profiles[] } → ~/.funnel/settings.json (global)
283
- or ~/.funnel/projects/<id>/settings.json (per-repo funnel.json)
286
+ Settings = { channels[], profiles[] } → ~/.funnel/settings.json(グローバル)
287
+ または ~/.funnel/projects/<id>/settings.json(リポジトリ単位の funnel.json
284
288
  ```
285
289
 
286
- ## File layout
290
+ ### ファイルレイアウト
287
291
 
288
- Persistent state lives under `~/.funnel/`. Volatile logs and the event log live under `/tmp/funnel/`.
292
+ 永続データは `~/.funnel/` 配下、揮発ログとイベントログは `/tmp/funnel/` 配下に住む。
289
293
 
290
294
  ```
291
295
  ~/.funnel/
292
- ├── settings.json global channels[] with nested connectors, profiles[]
296
+ ├── settings.json グローバルの channels[]nested connectors), profiles[]
293
297
  ├── projects/
294
- │ └── <id>/ per-repo state for a repo with funnel.json
295
- │ └── settings.json, gateway.token, claude/, ... (same layout as the global root, scoped by funnel.json id)
296
- ├── gateway.pid daemon PID
297
- ├── gateway.token Bearer token for daemon HTTP / WS
298
+ │ └── <id>/ funnel.json を持つリポジトリのスコープ state
299
+ │ └── settings.json, gateway.token, claude/, ... (グローバルと同じレイアウト、funnel.json id でスコープ)
300
+ ├── gateway.pid デーモン PID
301
+ ├── gateway.token デーモン HTTP / WS Bearer token
298
302
  ├── claude/
299
- │ └── <profile-id>.pid prevents double-launch of the same profile (keyed by profile id)
303
+ │ └── <profile-id>.pid 同一プロファイルの二重起動を防ぐ(profile id がキー)
300
304
  └── channels/
301
305
  └── <channel-id>/
302
306
  └── connectors/
303
307
  └── <connector-id>/
304
- └── state.json per-connector durable state (e.g. schedule lastFiredAt)
308
+ └── state.json コネクタごとの永続 state(例: schedule lastFiredAt
305
309
 
306
310
  /tmp/funnel/
307
- ├── events.db SQLite event log with replay-by-offset
308
- ├── funnel.log diagnostic log (daemon lifecycle, listener boot, connects)
309
- └── gateway.log daemon stdout/stderr
311
+ ├── events.db offset 再生付きの SQLite イベントログ
312
+ ├── funnel.log 診断ログ(デーモンの起動、listener boot、接続)
313
+ └── gateway.log デーモンの stdout/stderr
310
314
  ```
311
315
 
312
- Notes:
316
+ コネクタの設定は settings.json にインライン(チャネルの下に nested)で保存され、型ごとのディレクトリには置かない。コネクタごとの永続 state(schedule catch-up の `lastFiredAt` など)は `channels/<channel-id>/connectors/<connector-id>/state.json` に id をキーに住むので、rename しても state を失わない。`fnl gateway logs` は `funnel.log` を tail して YAML として描画する。
313
317
 
314
- - Connector configuration is stored inline in `settings.json` (nested under the channel), not in a per-type directory. Per-connector durable state (e.g. `lastFiredAt` for schedule catch-up) lives under `channels/<channel-id>/connectors/<connector-id>/state.json` keyed by id, so renames do not lose state.
315
- - `fnl gateway logs` tails `funnel.log` and renders it as YAML.
318
+ ### 環境変数
316
319
 
317
- ## Environment variables
320
+ - `FUNNEL_CHANNEL_ID` — `fnl claude` が子プロセスに注入する。funnel MCP がこれで購読する
321
+ - `FUNNEL_PORT` — gateway ポート。`funnel` CLI 起動はデフォルト 9743、プログラムから起こす gateway は 9742
322
+ - `FUNNEL_GATEWAY_URL` — MCP が WS 購読と HTTP 返信の両方で使うデーモンのベース URL(デフォルト `http://127.0.0.1:<port>`)
323
+ - `FUNNEL_GATEWAY_TOKEN` — デーモン HTTP / WS の Bearer token。デフォルトは `~/.funnel/gateway.token` の中身
318
324
 
319
- | Variable | Purpose |
320
- | ---------------------- | --------------------------------------------------------------------------------------------- |
321
- | `FUNNEL_CHANNEL_ID` | Injected into the child process by `fnl claude`; the funnel MCP uses it to subscribe. |
322
- | `FUNNEL_PORT` | Gateway port. Default 9743 for `funnel` CLI launches, 9742 for a programmatically hosted gateway. |
323
- | `FUNNEL_GATEWAY_URL` | Daemon base URL used by MCP for both WS subscribe and HTTP reply (default `http://127.0.0.1:<port>`). |
324
- | `FUNNEL_GATEWAY_TOKEN` | Bearer token for the daemon HTTP / WS. Defaults to the contents of `~/.funnel/gateway.token`. |
325
+ ## Discord bot のセットアップ
325
326
 
326
- ## Discord bot setup
327
+ - Discord Developer Portal で bot を作りトークンを取得する
328
+ - Privileged Gateway Intents の `Message Content Intent` を有効にする
329
+ - OAuth2 → URL Generator で `bot` スコープと `View Channels` / `Send Messages` / `Read Message History` 権限を付けて招待する
327
330
 
328
- - Create a bot in the Discord Developer Portal and obtain its token
329
- - Enable `Message Content Intent` under Privileged Gateway Intents
330
- - Invite the bot via OAuth2 → URL Generator with the `bot` scope and `View Channels` / `Send Messages` / `Read Message History` permissions
331
+ ## プログラマブル API(Bun)
331
332
 
332
- ## Programmable API (Bun)
333
-
334
- `funnel` is also usable as a library — the same `Funnel` facade the CLI uses is exported from the package root. The constructor is fully lazy: `new Funnel()` records its props and freezes; no disk / process / network access happens until a method is called.
333
+ CLI を介さず、ライブラリとして組み込める。CLI が使うのと同じ `Funnel` facade をパッケージのルートから export している。コンストラクタは lazy で、`new Funnel()` は props を記録して freeze するだけ。メソッドを呼ぶまでディスク / プロセス / ネットワークに触れない。
335
334
 
336
335
  ```ts
337
336
  import { Funnel } from "@interactive-inc/claude-funnel"
338
337
 
339
- const funnel = new Funnel() // defaults to ~/.funnel + /tmp/funnel on the local filesystem
340
-
341
- funnel.paths
342
- // → { dir: "/Users/you/.funnel", tmpDir: "/tmp/funnel", settings: "/Users/you/.funnel/settings.json" }
338
+ const funnel = new Funnel() // ~/.funnel + /tmp/funnel がデフォルト
343
339
 
344
340
  const channel = funnel.channels.add({ name: "inbox" })
345
-
346
341
  funnel.channels.addConnector("inbox", {
347
342
  type: "slack",
348
343
  name: "my-slack",
349
344
  botToken: "xoxb-...",
350
345
  appToken: "xapp-...",
351
346
  })
352
-
353
- for (const c of funnel.channels.list()) console.log(c.name, c.connectors.length)
354
347
  ```
355
348
 
356
- Every facet — `channels` / `profiles` / `gateway` / `gatewayServer` / `gatewayToken` / `listeners` / `mcp` / `claude` / `factory` / `store` / `process` / `logger` / `paths` — is reachable from the same instance:
349
+ `channels` / `profiles` / `gateway` / `listeners` / `mcp` / `claude` など全ファセットが同じインスタンスから辿れる。`gateway` はデーモンの起動・停止、`listeners` は動作中デーモンとの HTTP 会話、`claude` はエージェント起動を担う。
357
350
 
358
351
  ```ts
359
- funnel.gateway.getStatus() // { running, pid, port }
360
- await funnel.gateway.start() // spawns the daemon as a separate process
352
+ await funnel.gateway.start() // デーモンを別プロセスとして spawn
353
+ funnel.gateway.getStatus() // { running, pid, port }
361
354
 
362
- await funnel.listeners.list() // talks to the running daemon over HTTP
363
355
  await funnel.listeners.start("inbox", "my-slack")
364
356
  await funnel.listeners.restart("inbox", "my-slack")
365
357
 
366
- await funnel.claude.launch({ channel: "inbox" })
367
- funnel.mcp.install("/path/to/repo") // writes .mcp.json
358
+ await funnel.claude.launch({ channel: "inbox" }) // claude を起動(.mcp.json も自動で書く)
368
359
  ```
369
360
 
370
- Run the gateway in-process (no daemon spawn — useful for tests or embedding):
361
+ デーモンを spawn せず、gateway をインプロセスで動かすこともできる(テストや埋め込み向け)。`onEvent` で全 broadcast イベントをインプロセスで観測できる。
371
362
 
372
363
  ```ts
373
364
  const server = funnel.gatewayServer({ port: 9742 })
374
- await server.start() // Bun.serve (HTTP + WS) + listener supervisor
365
+ await server.start() // Bun.serve (HTTP + WS) + listener supervisor
375
366
  const unsubscribe = server.onEvent(({ content, meta }) => {
376
- console.log(meta?.connector, content) // in-process observer for every broadcast event
367
+ console.log(meta?.connector, content)
377
368
  })
378
369
  await server.stop()
379
370
  unsubscribe()
380
371
  ```
381
372
 
382
- Persistence and replay live behind the `FunnelEventLog` port. The default is a `SqliteFunnelEventLog` (durable across daemon restarts: it seeds the broadcaster's offset and serves reconnect replay). Inject `MemoryFunnelEventLog` or any `FunnelEventLog` — to swap or disable durable replay; `onEvent` is a separate, write-only observation hook and does not replace it:
373
+ 永続化と再生は `FunnelEventLog` port の裏にある。デフォルトは `SqliteFunnelEventLog`(デーモン再起動を跨いで durable。reconnect 時の再生を提供する)。`gatewayServer({ eventLog })` `MemoryFunnelEventLog` を渡せば durable な再生を差し替え・無効化できる。`onEvent` は書き込み専用の観測フックで、再生(読み戻し)は EventLog の責務。
383
374
 
384
- ```ts
385
- import { MemoryFunnelEventLog } from "@interactive-inc/claude-funnel"
375
+ ### テスト用のサンドボックス
386
376
 
387
- const server = funnel.gatewayServer({ port: 9742, eventLog: new MemoryFunnelEventLog() })
388
- ```
389
-
390
- The daemon exposes `/health`, `/status`, `/listeners*`, `/channels/:channel/connectors/:connector/call`, plus the `/ws?channel=<name>` WebSocket.
391
-
392
- ### Sandboxed Funnel
393
-
394
- `Funnel.inMemory()` returns a Funnel pre-wired with Memory implementations for every IO boundary — useful for tests and ad-hoc experiments. Pass any subset of `props` to override individual seams:
377
+ `Funnel.inMemory()` は全 IO 境界(ディスク / プロセス / clock / UUID)を Memory 実装で配線済みの Funnel を返す。`props` の任意の部分集合で個々の seam を上書きできるので、実 FS や spawn に触れずにテストを書ける。
395
378
 
396
379
  ```ts
397
- import { Funnel } from "@interactive-inc/claude-funnel"
398
-
399
- const funnel = Funnel.inMemory() // touches no real disk, processes, clock, or UUIDs
400
- funnel.channels.add({ name: "inbox" }) // mutates the in-memory store
380
+ const funnel = Funnel.inMemory() // 実ディスク / プロセス / clock / UUID に触れない
381
+ funnel.channels.add({ name: "inbox" }) // インメモリ store を変更する
401
382
  ```
402
383
 
403
- The longhand form (for fine-grained control) is still available:
404
-
405
- ```ts
406
- import {
407
- Funnel,
408
- MemoryFunnelClock,
409
- MemoryFunnelFileSystem,
410
- MemoryFunnelIdGenerator,
411
- MemoryFunnelLogger,
412
- MemoryFunnelProcessRunner,
413
- MockFunnelSettingsReader,
414
- } from "@interactive-inc/claude-funnel"
415
-
416
- const funnel = new Funnel({
417
- store: new MockFunnelSettingsReader(),
418
- fs: new MemoryFunnelFileSystem(),
419
- process: new MemoryFunnelProcessRunner(),
420
- logger: new MemoryFunnelLogger(),
421
- clock: new MemoryFunnelClock({ start: new Date("2026-01-01T00:00:00Z") }),
422
- idGenerator: new MemoryFunnelIdGenerator({ prefix: "test" }),
423
- dir: "/sandbox/.funnel",
424
- tmpDir: "/sandbox/tmp",
425
- })
426
- ```
427
-
428
- Available abstractions (each has `Funnel*` interface, `Node*` default, and `Memory*` for tests): `FunnelFileSystem`, `FunnelProcessRunner`, `FunnelLogger`, `FunnelClock`, `FunnelIdGenerator`. Plus `NoopFunnelLogger` for silent operation and `MockFunnelSettingsReader` for an in-memory settings store.
429
-
430
- ### Embedding the CLI
431
-
432
- The same Hono app that backs `fnl` is published as `createCliApp(funnel)` — pass any `Funnel` instance to bind a custom store / boundaries to the routes. The pair `toRequest` (argv → request) and `queryToCliArgs` (URL search params → CLI flags) lets you drive the app programmatically:
433
-
434
- ```ts
435
- import { Funnel, createCliApp, toRequest } from "@interactive-inc/claude-funnel"
436
-
437
- const app = createCliApp(Funnel.inMemory())
438
- const { method, url } = toRequest(["channels", "add", "inbox"])
439
- const res = await app.request(url, { method })
440
- console.log(await res.text())
441
- ```
442
-
443
- `cliApp` is the same app pre-wired to `new Funnel()` for callers who just want the default. The middleware sets the chosen Funnel onto `c.var.funnel`; the matching `Env` type is exported for composing custom routes that share the same context variable.
444
-
445
- ### Validating connector configs
446
-
447
- Each connector type publishes its Zod schema, so consumers can parse external configs (JSON files, API payloads, etc.) before handing them to `addConnector`. The discriminated union `connectorConfigSchema` covers the whole set.
448
-
449
- ```ts
450
- import {
451
- connectorConfigSchema,
452
- slackConnectorSchema,
453
- type SlackConnectorConfig,
454
- } from "@interactive-inc/claude-funnel"
455
-
456
- const slack: SlackConnectorConfig = slackConnectorSchema.parse(json)
457
- const any = connectorConfigSchema.parse(json) // narrows by `type`
458
- ```
459
-
460
- ### Packaging
461
-
462
- The published package ships a bundled library entry (`dist/index.js`) plus generated declarations (`dist/**/*.d.ts`), so consumers do not need a matching tsconfig paths setup to resolve `@/...` imports. The `fnl` / `funnel` bin entries point to a separately bundled `dist/bin.js`. Import `@interactive-inc/claude-funnel/bin` only if you are embedding the CLI binary rather than the library.
384
+ `fnl` を支える Hono アプリ(`createCliApp` / `toRequest`)や、各コネクタの Zod スキーマ(`connectorConfigSchema`)も export している。詳細は型定義を参照。
463
385
 
464
386
  ## Claude Code skill
465
387
 
466
- This repo ships a Claude Code skill at `.claude/skills/funnel/SKILL.md`. It briefs Claude on the architecture and command groups, and tells it to defer flag-level details to `funnel <command> --help`.
388
+ このリポジトリは `.claude/skills/funnel/SKILL.md` Claude Code skill を同梱している。アーキテクチャとコマンド群を Claude に説明し、フラグレベルの詳細は `funnel <command> --help` に委ねるよう指示する。
467
389
 
468
- Project-scoped (auto): if you run `claude` inside this repo, the skill is picked up automatically — no install step.
390
+ プロジェクトスコープ(自動): このリポジトリ内で `claude` を実行すると skill が自動で読まれる。インストール手順は不要。
469
391
 
470
- Global (use the skill in any project): Claude Code does not currently provide a CLI to install skills from a remote URL, so copy the file into your personal skills directory:
392
+ グローバル(任意のプロジェクトで使う): Claude Code はリモート URL から skill をインストールする CLI を今のところ提供していないので、ファイルを個人の skills ディレクトリへコピーする。
471
393
 
472
394
  ```bash
473
- # from a clone of this repo
395
+ # このリポジトリの clone から
474
396
  mkdir -p ~/.claude/skills/funnel
475
397
  cp .claude/skills/funnel/SKILL.md ~/.claude/skills/funnel/
476
398
  ```
477
399
 
478
- Or fetch it directly without cloning:
400
+ clone せず直接取得することもできる。
479
401
 
480
402
  ```bash
481
403
  mkdir -p ~/.claude/skills/funnel
@@ -483,31 +405,31 @@ curl -fsSL https://raw.githubusercontent.com/interactive-inc/open-claude-funnel/
483
405
  -o ~/.claude/skills/funnel/SKILL.md
484
406
  ```
485
407
 
486
- After this, Claude Code will load the skill in any session.
408
+ これで Claude Code がどのセッションでも skill を読む。
487
409
 
488
- ## Development
410
+ ## 開発
489
411
 
490
412
  ```bash
491
413
  git clone https://github.com/interactive-inc/open-claude-funnel.git
492
414
  cd open-claude-funnel
493
- bun install # install deps (no auto-build)
494
- make build # produce dist/ — run this once after install
495
- bun link # symlinks fnl / funnel → dist/bin.js
496
- make build # rebuild library + CLI after editing
497
- make build-lib # library only (vp pack)
498
- make build-bin # CLI / daemon only (bun build --minify)
499
- make clean # remove dist/
500
- bun test # run tests
501
- bunx tsc -b # type check
502
- bun lib/bin.ts ... # run the cli from source (no build) for fast iteration
415
+ bun install # 依存インストール(自動ビルドなし)
416
+ make build # dist/ を生成 install 後に一度実行
417
+ bun link # fnl / funnel → dist/bin.js を symlink
418
+ make build # 編集後にライブラリ + CLI を再ビルド
419
+ make build-lib # ライブラリのみ(vp pack
420
+ make build-bin # CLI / daemon のみ(bun build --minify
421
+ make clean # dist/ を削除
422
+ bun test # テスト実行
423
+ bunx tsc -b # 型チェック
424
+ bun lib/bin.ts ... # ソースから CLI を実行(ビルド不要、高速イテレーション)
503
425
  ```
504
426
 
505
- ## Links
427
+ ## リンク
506
428
 
507
429
  - [GitHub](https://github.com/interactive-inc/open-claude-funnel)
508
430
  - [Issues](https://github.com/interactive-inc/open-claude-funnel/issues)
509
- - Coding rules and design principles: [CLAUDE.md](https://github.com/interactive-inc/open-claude-funnel/blob/main/CLAUDE.md)
431
+ - コーディング規約と設計原則: [CLAUDE.md](https://github.com/interactive-inc/open-claude-funnel/blob/main/CLAUDE.md)
510
432
 
511
- ## License
433
+ ## ライセンス
512
434
 
513
435
  MIT © Interactive Inc.